@explorer02/cfm-survey-sdk 0.2.3 → 0.2.4

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.
Files changed (94) hide show
  1. package/dist/cli/index.js +140 -33
  2. package/dist/cli/index.mjs +140 -33
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/package.json +8 -1
  6. package/postinstall.js +8 -5
  7. package/templates/AGENT.md +60 -19
  8. package/templates/deploy-wizard-to-s3.sh +52 -0
  9. package/templates/docs/00-integration/agent-execution-flow.md +281 -0
  10. package/templates/docs/00-integration/agent-operating-contract.md +260 -0
  11. package/templates/docs/00-integration/apply-ui-config.md +365 -0
  12. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  13. package/templates/docs/00-integration/client-integration-guide.md +9 -7
  14. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  15. package/templates/docs/00-integration/component-checklist.md +88 -62
  16. package/templates/docs/00-integration/constraints.md +13 -5
  17. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  18. package/templates/docs/00-integration/progress.md +4 -2
  19. package/templates/docs/00-integration/setup.md +12 -0
  20. package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
  21. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  22. package/templates/docs/00-integration/wizard-api.md +134 -0
  23. package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
  24. package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
  25. package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
  26. package/templates/docs/01-components/01-survey-page.md +18 -8
  27. package/templates/docs/01-components/02-question.md +2 -2
  28. package/templates/docs/01-components/03-rating-scale.md +12 -18
  29. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  30. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -39
  31. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  32. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  33. package/templates/docs/01-components/10-header-footer.md +23 -4
  34. package/templates/docs/01-components/11-progress-bar.md +14 -6
  35. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  36. package/templates/docs/01-components/README.md +2 -2
  37. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  38. package/templates/docs/02-reference/routing-table.md +1 -0
  39. package/templates/docs/02-reference/value-derivation.md +1 -1
  40. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  41. package/templates/docs/03-ui-specs/01-rating.md +18 -0
  42. package/templates/docs/03-ui-specs/02-radio.md +16 -1
  43. package/templates/docs/03-ui-specs/03-text.md +12 -1
  44. package/templates/docs/03-ui-specs/04-csat.md +84 -50
  45. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  46. package/templates/docs/03-ui-specs/07-matrix-cfm.md +71 -48
  47. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +48 -18
  48. package/templates/docs/03-ui-specs/09-slider-matrix.md +59 -32
  49. package/templates/docs/03-ui-specs/10-file-upload.md +11 -0
  50. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  51. package/templates/docs/03-ui-specs/12-survey-chrome.md +54 -5
  52. package/templates/docs/03-ui-specs/13-heatmap.md +12 -1
  53. package/templates/docs/03-ui-specs/14-rank-order.md +11 -0
  54. package/templates/docs/03-ui-specs/README.md +4 -2
  55. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  56. package/templates/docs/MANIFEST.json +162 -6
  57. package/templates/docs/index.md +116 -117
  58. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  59. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  60. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  61. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  62. package/templates/docs/templates/Question.tsx +3 -0
  63. package/templates/docs/templates/RatingScale.tsx +129 -0
  64. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  65. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  66. package/templates/docs/templates/customFieldValues.ts +36 -0
  67. package/templates/docs/templates/implementation_plan.md +56 -9
  68. package/templates/docs/templates/labelStyles.ts +33 -0
  69. package/templates/docs/templates/selectionStyles.ts +100 -0
  70. package/templates/docs/templates/surveyUiIcons.tsx +11 -0
  71. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  72. package/templates/docs/templates/verify-agent-build.sh +119 -0
  73. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  74. package/templates/preview-harness/previewPages.js +108 -0
  75. package/templates/preview-harness/previewPages.ts +298 -0
  76. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  77. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  78. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  79. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  80. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  81. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  82. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  83. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  84. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  85. package/templates/previewBridge.ts +153 -0
  86. package/templates/survey-theme.css +295 -0
  87. package/templates/survey-ui-config.schema.json +213 -0
  88. package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
  89. package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
  90. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  91. package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
  92. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  93. package/templates/wizard-dist/index.html +20 -0
  94. package/templates/wizard.html +13 -1129
@@ -48,5 +48,6 @@ import { QUESTION_TYPE } from '@explorer02/cfm-survey-sdk';
48
48
  | `CustomSliderTrack` | `01-components/09-custom-slider-track.md` |
49
49
  | `Header` / `Footer` | `01-components/10-header-footer.md` |
50
50
  | `ProgressBar` | `01-components/11-progress-bar.md` |
51
+ | `SurveyStickyChrome` | `01-components/19-survey-sticky-chrome.md` |
51
52
  | `LanguageSelector` | `01-components/12-language-selector.md` |
52
53
  | `MatrixDropdown` | `01-components/13-matrix-dropdown.md` |
@@ -24,7 +24,7 @@ type ScaleColumn = { id: string; label: string };
24
24
  - Filter N/A from scale pickers: `scaleColumns.filter(col => col.id !== 'na')`
25
25
  - Slider matrix rows still store numeric slider positions (unchanged)
26
26
 
27
- Monorepo client mirror: `apps/client/src/lib/surveyUi/scaleValues.ts` exports the same helper for matrix UI components.
27
+ Portable template: [`templates/surveyUiScaleUtils.ts`](../templates/surveyUiScaleUtils.ts) exports `matrixColumnStoredValue` (same contract). Copy to `src/lib/` — see [`client-lib-folder.md`](../00-integration/client-lib-folder.md).
28
28
 
29
29
  ## NPS / star rating options
30
30
 
@@ -31,3 +31,13 @@ Never use a `default` fallback that shows `"not yet implemented"`.
31
31
 
32
32
  - Answers: `state.answers` (not `state.values`)
33
33
  - Change action: `{ type: 'CHANGE', payload: { questionId, answerValue } }`
34
+
35
+ ## Wizard customization (Phase 5 — required)
36
+
37
+ Question shell inherits global theme tokens — no per-shell hex overrides:
38
+
39
+ - Validation banner: `--cfm-validation-border`, `--cfm-validation-bg`
40
+ - Question title: `--cfm-question-title-color`
41
+ - Required asterisk: `--cfm-required-color`
42
+
43
+ See [`wizard-preview-build-guide.md`](../00-integration/wizard-preview-build-guide.md) for `data-cfm-*` on wrapper-adjacent chrome only.
@@ -44,3 +44,21 @@ See [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
44
44
  - [ ] Two-row grid with `gridTemplateColumns: repeat(options.length, 1fr)`
45
45
  - [ ] Only track row is clickable
46
46
  - [ ] min/mid/max labels in Question wrapper with midLabelIndex formula
47
+
48
+ ## Wizard customization (Phase 5 — required)
49
+
50
+ > Reference hex in this spec are defaults. Wire `var(--cfm-*)` per [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md). Copy [`selectionStyles.ts`](../templates/selectionStyles.ts) and [`labelStyles.ts`](../templates/labelStyles.ts).
51
+
52
+ | Control | CSS vars | Apply in |
53
+ |---------|----------|----------|
54
+ | Track bar / highlight | `--cfm-nps-track-bar`, `--cfm-nps-track-highlight`, `--cfm-nps-selected-fill` | `RatingScale.tsx` track row |
55
+ | Hint labels (min/max) | `--cfm-hint-label-color`, `--cfm-hint-label-bg` | `hintLabelStyle()` on anchor row |
56
+ | Number labels | `--cfm-number-label-mode`, `--cfm-number-mono-color`, `--cfm-number-color-{n}` | Badge row — unselected only |
57
+ | Selection | `--cfm-matrix-selected`, `--cfm-input-focus-ring` | `scaleRadioRingStyle` / `scaleRadioDotStyle` |
58
+
59
+ ### Wizard checklist
60
+
61
+ - [ ] `selectionStyles.ts` imported in `RatingScale.tsx`
62
+ - [ ] Selected radio: outer border + dot = focus ring; annulus = cell selected
63
+ - [ ] Selected numbered badge: border = focus ring, fill = cell selected, text white
64
+ - [ ] Unselected badges keep traffic-light / per-number `--cfm-number-*` colors
@@ -92,9 +92,24 @@ onAction({ type: 'CHANGE', payload: { questionId, answerValue: string[] } })
92
92
 
93
93
  - [ ] Use `selectionMode`, not `isMultiSelect`
94
94
  - [ ] Full-width card options; custom circular radio / checkbox indicators
95
- - [ ] Selected: magenta border `#e20074` + pink bg `#fdf2f8`
95
+ - [ ] Selected: use `var(--cfm-input-focus-ring)` border + `var(--cfm-mcq-selected-bg)` / `var(--cfm-matrix-selected)` not hardcoded `#e20074` / `#fdf2f8`
96
96
  - [ ] Multi-select: `string[]` answer; block at `maxSelections`
97
97
  - [ ] Defaults: bind to `state.answers`; pre-selected visual on load when `defaultOptionIds` set
98
98
  - [ ] `getVisibleMcqOptionsForAnswers` with optional `customFieldValues` 4th arg
99
99
  - [ ] Store `option.id` — never label text
100
100
  - [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../00-integration/question-type-sdk-matrix.md#mcq)
101
+
102
+ ## Wizard customization (Phase 5 — required)
103
+
104
+ > Wire MCQ tokens per [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md). Copy [`selectionStyles.ts`](../templates/selectionStyles.ts).
105
+
106
+ | Control | CSS vars | Apply in |
107
+ |---------|----------|----------|
108
+ | Option gap / radius / padding | `--cfm-mcq-option-gap`, `--cfm-mcq-border-radius`, `--cfm-mcq-card-padding` | MCQ option cards |
109
+ | Selection (radio) | `--cfm-input-focus-ring`, `--cfm-matrix-selected`, `--cfm-mcq-selected-bg` | Outer ring border + annulus + inner dot |
110
+ | Selection (checkbox) | `--cfm-input-focus-ring`, `--cfm-mcq-selected-bg` | Checkbox border + fill |
111
+
112
+ ### Wizard checklist
113
+
114
+ - [ ] Single-select radio uses ring + annulus + dot pattern from `selectionStyles.ts`
115
+ - [ ] `data-cfm-mcq-style` on `:root` when `questionTypes.MCQ.optionStyle` set
@@ -1,6 +1,6 @@
1
1
  # UI Spec: Text Field (`type: 'TEXTFIELD'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/Question.tsx` (inline)
3
+ > **Reference:** portable `docs/templates/Question.tsx` (inline TEXTFIELD branch)
4
4
  > **SDK type:** `TextInputQuestion` — `fetchSurvey/types/text.ts`
5
5
 
6
6
  ## Config Inventory
@@ -49,3 +49,14 @@ onSelect(string) // onChange from textarea
49
49
  - [ ] Character counter bottom-right
50
50
  - [ ] Magenta focus ring
51
51
  - [ ] Implement `inputVariant: 'short'` as single-line input when present
52
+
53
+ ## Wizard customization (Phase 5 — required)
54
+
55
+ | Control | CSS vars | Apply in |
56
+ |---------|----------|----------|
57
+ | Input radius / border / placeholder | `--cfm-input-radius`, `--cfm-input-border`, `--cfm-input-placeholder` | `textarea` / `input` |
58
+ | Focus outline | `--cfm-input-focus-ring` | `:focus-visible` ring |
59
+
60
+ ### Wizard checklist
61
+
62
+ - [ ] No hardcoded magenta focus hex — use `var(--cfm-input-focus-ring)`
@@ -1,6 +1,7 @@
1
1
  # UI Spec: CSAT Matrix Grid (`type: 'CSAT_MATRIX'`)
2
2
 
3
- > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/CsatMatrixScale.tsx`
3
+ > **COPY TEMPLATE FIRST:** [`templates/CsatMatrixScale.tsx`](../templates/CsatMatrixScale.tsx) + [`surveyUiIcons.tsx`](../templates/surveyUiIcons.tsx) + [`surveyUiScaleUtils.ts`](../templates/surveyUiScaleUtils.ts) + [`CustomSliderTrack.tsx`](../templates/CustomSliderTrack.tsx) + [`MatrixDropdown.tsx`](../templates/MatrixDropdown.tsx)
4
+ >
4
5
  > **SDK type:** `CsatQuestion` — `fetchSurvey/types/csat.ts`
5
6
  > **Component blueprint:** `01-components/05-csat-matrix-scale.md`
6
7
 
@@ -9,75 +10,89 @@
9
10
  | Field | Implemented | UI impact |
10
11
  |-------|-------------|-----------|
11
12
  | `statementRows[]`, `scaleColumns[]` | ✅ | Grid / carousel / dropdown data |
12
- | `displayStyle` | ✅ | Grid vs vertical list routing |
13
+ | `displayStyle` | ✅ | emoji / star / numbered / graphics / standard |
13
14
  | `gridLayout` | ✅ | carousel vs standard vs dropdown |
14
- | `hasNotApplicableOption` | ✅ | Checkbox below row (vertical) or N/A column (grid) |
15
+ | `hasNotApplicableOption` | ✅ | N/A column (56px) or vertical checkbox |
15
16
  | `reverseScaleOrder` | ✅ | Flip emoji/star/number index |
16
- | `scaleAnchorLabels[]` | ✅ | Anchor row above columns |
17
+ | `scaleAnchorLabels[]` | ✅ | Anchor row above columns (replaces column headers when set) |
17
18
 
18
- ## Config Decision Tree
19
+ ## Statement & label visibility (CRITICAL)
19
20
 
20
- ```
21
- CSAT_MATRIX
22
- ├── gridLayout === 'carousel' CsatMatrixCarousel
23
- ├── displayStyle in (dropdown, selectbox) → CsatMatrixVerticalList
24
- │ ├── dropdown MatrixDropdown placeholder "Select..."
25
- │ ├── selectbox → full-width cards #fdf2f8 selected
26
- │ └── else → card radio list
27
- └── else CsatMatrixGrid
28
- ├── displayStyle === 'emoji' → client-side emoji icons (react-icons) + hover scale + ring when selected
29
- ├── displayStyle === 'star' opacity 0.5 unselected; filled/empty icons
30
- ├── displayStyle === 'numbered' → 40×40 badge; selected magenta fill
31
- └── displayStyle === 'graphics' → CustomSliderTrack per row (carousel)
21
+ Agents often render inputs with **no row labels** and **no column headers**. Every matrix grid MUST include:
22
+
23
+ ### 1. Row labels `statementRows[].statementText`
24
+
25
+ Fixed **180px** left column on every data row:
26
+
27
+ ```tsx
28
+ <div style={{ width: '180px', flexShrink: 0, padding: '0 16px' }}>
29
+ <span
30
+ style={{ fontSize: '14px', fontWeight: 500, color: '#111827' }}
31
+ dangerouslySetInnerHTML={{ __html: row.statementText }}
32
+ />
33
+ </div>
32
34
  ```
33
35
 
34
- ## Grid Structure
36
+ ### 2. Column labels — two modes (mutually exclusive header row)
35
37
 
36
- - Row label: `180px` fixed column
37
- - N/A column: `56px` when `hasNotApplicableOption`
38
- - Alternating row backgrounds
39
- - Column headers OR `scaleAnchorLabels[]` anchor row with percentage positioning
38
+ | Condition | What to render above cells |
39
+ |-----------|---------------------------|
40
+ | `scaleAnchorLabels.length > 0` | Anchor label row positioned over columns (see formula below) |
41
+ | No anchor labels | Column header row from `scaleColumns[].label` |
40
42
 
41
- ## Carousel Implementation Recipe
43
+ **When `scaleAnchorLabels` is set**, still render anchor row even if column text labels are long (emoji mode Page 9 fixture):
42
44
 
43
- Same rules as CFM carousel (prev/next disabled at bounds, dot click jumps row, no auto-advance). CSAT uses **pink** active dot `#e20074` (not Likert blue).
45
+ ```typescript
46
+ const m = scaleColumns.length;
47
+ const n = scaleAnchorLabels.length;
48
+ const percent = n === 1 ? 50 : (0.5 / m + (i / (n - 1)) * ((m - 1) / m)) * 100;
49
+ // position label i at left: `${percent}%`, transform: translateX(-50%)
50
+ ```
44
51
 
45
- ## N/A mutex
52
+ **When no anchor labels**, render column headers:
46
53
 
47
- | Layout | Behavior |
48
- |--------|----------|
49
- | Grid N/A column | Selecting N/A clears row; selecting column clears N/A |
50
- | Vertical checkbox | `hasNotApplicableOption` checkbox below row sets `null` |
54
+ ```tsx
55
+ {scaleColumns.map(col => (
56
+ <span dangerouslySetInnerHTML={{ __html: col.label }} />
57
+ ))}
58
+ ```
51
59
 
52
- ## Emoji / star / numbered — react-icons mapping
60
+ ### 3. N/A column header
53
61
 
54
- Copy [`templates/surveyUiIcons.tsx`](../templates/surveyUiIcons.tsx) or implement `getEmojiForIndex(index, totalColumns)`.
62
+ When `hasNotApplicableOption`, reserve **56px** right column with "N/A" header text.
55
63
 
56
- **5-point CSAT emoji index → icon key:**
64
+ ## Config Decision Tree
57
65
 
58
- | Column index (0-based) | Sentiment | Maps to 11-point scale key |
59
- |--------------------------|-----------|----------------------------|
60
- | 0 | Very negative | 3 |
61
- | 1 | Negative | 4 |
62
- | 2 | Neutral | 6 |
63
- | 3 | Positive | 7 |
64
- | 4 | Very positive | 9 |
66
+ ```
67
+ CSAT_MATRIX
68
+ ├── gridLayout === 'carousel' CsatMatrixCarousel
69
+ ├── displayStyle in (dropdown, selectbox) CsatMatrixVerticalList
70
+ └── else CsatMatrixGrid
71
+ ├── displayStyle === 'emoji' getEmojiForIndex(colIdx, m) — NOT radio circles
72
+ ├── displayStyle === 'star' CsatStarIcons filled/empty
73
+ ├── displayStyle === 'numbered' → 40×40 magenta badges (colIdx+1)
74
+ ├── displayStyle === 'graphics' → CustomSliderTrack per row
75
+ └── displayStyle === 'standard' → custom radio circles #e20074
76
+ ```
65
77
 
66
- **Star mode:** filled `FaStar` `#e20074` when selected; `FaRegStar` `#d1d5db` opacity 0.5 when unselected; hover ring `ring-2 ring-[#e20074] ring-offset-2`.
78
+ ## Emoji + anchor labels (Page 9 fixture)
67
79
 
68
- **Numbered mode:** 40×40 badge; selected = magenta fill `#e20074` white text; unselected = gray border.
80
+ - 5 `scaleAnchorLabels` over 6 columns (5 emoji + N/A)
81
+ - Emoji cells: pink ring when selected; tooltip shows `col.label` on hover
82
+ - Do NOT replace emojis with plain radio buttons
69
83
 
70
- **Graphics mode:** `CustomSliderTrack` per row with `ticks={scaleColumns.length}`, `tickLabels={scaleColumns.map(c => c.label)}`, `sliderType="graphics"`.
84
+ ## Star mode (Page 14 fixture)
71
85
 
72
- Apply `reverseScaleOrder` by flipping column index before icon lookup.
86
+ - `CsatStarIcons` from `surveyUiIcons.tsx`
87
+ - Unselected opacity 0.5; selected filled star `#e20074`
73
88
 
74
- ## Scale column labels on slider (Day 19)
89
+ ## Graphics / numbered (RATING_MATRIX reuse)
75
90
 
76
- Pass `tickLabels={visibleScaleColumns.map(col => col.label)}` to `CustomSliderTrack` so tooltips and graphics thumb show mapped labels, not raw numeric indices.
91
+ See [`08-matrix-csat-rating.md`](08-matrix-csat-rating.md) same `CsatMatrixScale` component.
77
92
 
78
93
  ## Answer Mutation
79
94
 
80
- Store **`column.id`** via `matrixColumnStoredValue(col)` — see `02-reference/value-derivation.md`.
95
+ Store **`column.id`** via `matrixColumnStoredValue(col)`:
81
96
 
82
97
  ```typescript
83
98
  onSelect(MatrixRowAnswers)
@@ -86,7 +101,26 @@ onAction({ type: 'CHANGE', payload: { questionId, answerValue: MatrixRowAnswers
86
101
 
87
102
  ## Agent Checklist
88
103
 
89
- - [ ] All displayStyle × gridLayout branches
90
- - [ ] reverseScaleOrder on emoji/star/numbered
91
- - [ ] Carousel pink dot indicators
92
- - [ ] Vertical selectbox cards match MCQ card styling
104
+ - [ ] Copied `CsatMatrixScale.tsx` + dependencies
105
+ - [ ] **180px statement column visible** on every row
106
+ - [ ] Column headers OR `scaleAnchorLabels` row visible (never both missing)
107
+ - [ ] `displayStyle` branch matches survey (emoji star ≠ numbered ≠ radio)
108
+ - [ ] N/A column when `hasNotApplicableOption`
109
+ - [ ] `react-icons` in package.json
110
+
111
+ ## Wizard customization (Phase 5 — required)
112
+
113
+ > Per-format controls in [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md). Copy [`selectionStyles.ts`](../templates/selectionStyles.ts) + [`labelStyles.ts`](../templates/labelStyles.ts).
114
+
115
+ | Format | Key tokens | Pattern |
116
+ |--------|------------|---------|
117
+ | emoji / star | `--cfm-input-focus-ring`, `cellSelectedVar` | Ring on select; star fill = cell selected |
118
+ | numbered | `--cfm-number-*`, focus ring + cell selected | Selected cell border + fill |
119
+ | radio grid | `--cfm-csat-column-label-*`, matrix radio styles | `csatColumnLabelStyle()` + `matrixRadioRingStyle` |
120
+ | graphics | `--cfm-slider-tick-label-*` | `CustomSliderTrack` thumb ring + annulus + dot |
121
+ | carousel | `--cfm-matrix-selected` | Active dot = cell selected |
122
+
123
+ ### Wizard checklist
124
+
125
+ - [ ] Column headers use `csatColumnLabelStyle()` — not hint labels
126
+ - [ ] No per-type accent hex overrides — use Question card tokens only
@@ -1,6 +1,8 @@
1
1
  # UI Spec: Standalone Slider (`type: 'slider'`)
2
2
 
3
- > **DEPRECATED:** Not in the current `SurveyQuestion` union. API SLIDER pages map to `SLIDER_MATRIX` or matrix `displayStyle: 'graphics'`. See `10-slider-matrix.md` and `CustomSliderTrack` specs.
3
+ > **⛔ DEPRECATED DO NOT IMPLEMENT.** API SLIDER maps to `SLIDER_MATRIX` (`09-slider-matrix.md`) or RATING/CSAT `displayStyle: 'graphics'` (`08-matrix-csat-rating.md`).
4
+ >
5
+ > If you see slider-like UI, copy [`templates/SliderMatrixScale.tsx`](../templates/SliderMatrixScale.tsx) or use `CsatMatrixScale` with `CustomSliderTrack` — not this spec.
4
6
  >
5
7
  > **Reference:** Standalone slider is not routed in current `Question.tsx`
6
8
  > **SDK type:** `SliderQuestion` — `fetchSurvey/types/slider.ts`
@@ -1,6 +1,7 @@
1
1
  # UI Spec: CFM Matrix (`type: 'CFM_MATRIX'`)
2
2
 
3
- > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/LikertMatrixScale.tsx`
3
+ > **COPY TEMPLATE FIRST:** [`templates/LikertMatrixScale.tsx`](../templates/LikertMatrixScale.tsx) + [`MatrixDropdown.tsx`](../templates/MatrixDropdown.tsx)
4
+ >
4
5
  > **SDK type:** `CfmMatrixQuestion` — `fetchSurvey/types/cfmMatrix.ts`
5
6
  > **Component blueprint:** `01-components/06-likert-matrix-scale.md`
6
7
 
@@ -18,68 +19,90 @@
18
19
  | `showColumnHeaders` | ✅ | Show/hide column header row |
19
20
  | `hasNotApplicableOption` | ✅ | N/A column with mutex |
20
21
 
21
- ## Config Decision Tree
22
+ ## Statement & label visibility (CRITICAL)
22
23
 
23
- ```
24
- CFM_MATRIX
25
- ├── gridLayout === 'dropdown' LikertMatrixDropdownLayout (MatrixDropdown per row)
26
- ├── gridLayout === 'carousel' → LikertMatrixCarouselLayout (one row, prev/next, blue dots)
27
- └── else LikertMatrixGridLayout
28
- ├── transposeTable ? columns×rows : rows×columns
29
- ├── repeatColumnHeaders ? header per row : single header
30
- ├── showColumnHeaders ? column headers : hidden
31
- └── statementLayout === 'bipolar' ? 25% | scale | 25% layout
32
- ```
24
+ ### Row labels
25
+
26
+ | Layout | Left column content | Width |
27
+ |--------|---------------------|-------|
28
+ | `statementLayout: 'likert'` | `statementRows[].statementText` | 180px |
29
+ | `statementLayout: 'bipolar'` | `statementRows[].statementText` | 25% |
30
+ | `transposeTable: true` | `scaleColumns[].label` (rows become scale items) | 180px |
33
31
 
34
- ## Interactive Behaviors
32
+ Always use `dangerouslySetInnerHTML` for HTML statement text.
35
33
 
36
- | Behavior | Condition | Action |
37
- |----------|-----------|--------|
38
- | Click selected radio again | single-select grid/carousel | Clear row (`undefined`) |
39
- | N/A selected | any | Other cols opacity 0.4, disabled |
40
- | Multi N/A | selectionMode multiple | Sets `[null]` exclusively |
41
- | Multi toggle | selectionMode multiple | Add/remove from array |
34
+ ### Bipolar right column
42
35
 
43
- ## Grid Layout
36
+ When `statementLayout === 'bipolar'`, render **`oppositeStatementText`** in 25% right column (unless transposed):
44
37
 
45
- - Row label column: `180px` (likert) or `25%` (bipolar)
46
- - Zebra rows: even rows `rgba(249,250,251,0.8)`
47
- - Native inputs: `accentColor: #e20074`, 18×18px
38
+ ```tsx
39
+ <span dangerouslySetInnerHTML={{ __html: row.oppositeStatementText ?? '' }} />
40
+ ```
48
41
 
49
- ## Carousel Implementation Recipe
42
+ ### Column / anchor labels
50
43
 
51
- | Rule | Behavior |
52
- |------|----------|
53
- | Prev button | Disabled when `currentRowIndex === 0` |
54
- | Next button | Disabled when on last visible row |
55
- | Dot indicators | Click dot → jump to that row index |
56
- | On answer | **Do not** auto-advance to next row |
57
- | Keyboard | Optional Left/Right for prev/next |
58
- | Active dot (Likert) | `#2563eb`, expanded pill `20×8px` vs `8×8px` inactive |
44
+ Header renders when `scaleAnchorLabels` OR `showColumnHeaders`:
59
45
 
60
- One statement row visible at a time; column headers below cells when `showColumnHeaders`.
46
+ **Bipolar + anchor labels (Page 7 fixture)** `showColumnHeaders: false`:
61
47
 
62
- ## N/A mutex (all layouts)
48
+ ```
49
+ [25% spacer] [Label1 at col2] [Label2 at col6] [Label3 at col10] [25% spacer]
50
+ [25% Left Stmt 1] [10 radios] [25% Right Stmt 1]
51
+ ```
63
52
 
64
- | Action | Result |
65
- |--------|--------|
66
- | Select N/A | Clear other column values for row; other inputs opacity 0.4 + disabled |
67
- | Select column after N/A | Clear N/A first |
68
- | Multi-select + N/A | N/A sets `[null]` exclusively |
53
+ Anchor positioning:
69
54
 
70
- ## Implementation Recipe
55
+ ```typescript
56
+ const numCols = gridCols.length;
57
+ if (idx === 0) left = `${100 / (2 * numCols)}%`;
58
+ else if (idx === labels.length - 1) left = `${100 - 100 / (2 * numCols)}%`;
59
+ else if (labels.length === 3 && idx === 1) left = '50%';
60
+ ```
71
61
 
72
- Store **`column.id`** via `matrixColumnStoredValue(col)`see `02-reference/value-derivation.md`.
62
+ **Transposed + column headers (Page 10 fixture)**`transposeTable: true`, `showColumnHeaders: true`:
73
63
 
74
64
  ```typescript
75
- onSelect(MatrixRowAnswers) // Record<statementRows[].id, value | value[] | null>
76
- onAction({ type: 'CHANGE', payload: { questionId, answerValue: MatrixRowAnswers } })
65
+ gridRows = scaleColumns; // row labels = Scale Item 1, 2, 3
66
+ gridCols = statementRows; // column headers = Statement Item 1, 2, 3
67
+ // Header cell HTML: col.statementText
68
+ // Row label HTML: row.label
69
+ input type = selectionMode === 'multiple' ? 'checkbox' : 'radio';
70
+ ```
71
+
72
+ Answer keys **always** use `statementRows[].id` as row id (`actualRowId` logic in template).
73
+
74
+ ## Config Decision Tree
75
+
76
+ ```
77
+ CFM_MATRIX
78
+ ├── gridLayout === 'dropdown' → MatrixDropdown per statement row
79
+ ├── gridLayout === 'carousel' → one row visible + blue dot nav
80
+ └── else → LikertMatrixGridLayout
81
+ ├── transposeTable swaps gridRows/gridCols
82
+ ├── showColumnHeaders controls column label row
83
+ └── statementLayout bipolar adds oppositeStatementText column
77
84
  ```
78
85
 
79
86
  ## Agent Checklist
80
87
 
81
- - [ ] All three gridLayout branches
82
- - [ ] transposeTable + repeatColumnHeaders + showColumnHeaders
83
- - [ ] Bipolar 25/50/25 layout with oppositeStatementText
84
- - [ ] Single-select deselect on re-click
85
- - [ ] N/A mutex with opacity 0.4
88
+ - [ ] Copied `LikertMatrixScale.tsx` + `MatrixDropdown.tsx`
89
+ - [ ] Row labels visible (statementText or transposed scale label)
90
+ - [ ] Bipolar: oppositeStatementText on right when not transposed
91
+ - [ ] Anchor labels OR column headers — never both missing when configured
92
+ - [ ] `selectionMode: multiple` uses checkboxes, not radios
93
+ - [ ] `transposeTable` swaps rows/cols correctly
94
+
95
+ ## Wizard customization (Phase 5 — required)
96
+
97
+ > CFM column headers are **label items** only. See [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md).
98
+
99
+ | Control | CSS vars | Apply in |
100
+ |---------|----------|----------|
101
+ | Row / bipolar width | `--cfm-matrix-row-width`, `--cfm-matrix-bipolar-width` | Statement columns |
102
+ | Label items | `--cfm-matrix-column-label-color`, `--cfm-matrix-column-label-bg` | Column header row — `matrixColumnLabelStyle()` |
103
+ | Selection | `--cfm-matrix-selected`, `--cfm-input-focus-ring` | `matrixRadioRingStyle` / dropdown option ring |
104
+
105
+ ### Wizard checklist
106
+
107
+ - [ ] `labelStyles.ts` imported; column headers never use hint-label vars
108
+ - [ ] No CFM-specific accent overrides — Question card tokens only
@@ -1,6 +1,7 @@
1
1
  # UI Spec: Rating Matrix Grid (`type: 'RATING_MATRIX'`)
2
2
 
3
- > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/CsatMatrixScale.tsx`
3
+ > **COPY TEMPLATE FIRST:** [`templates/CsatMatrixScale.tsx`](../templates/CsatMatrixScale.tsx) — **same component as CSAT_MATRIX**
4
+ >
4
5
  > **SDK type:** `RatingMatrixQuestion` — `fetchSurvey/types/ratingMatrix.ts`
5
6
  > **Component blueprint:** `01-components/05-csat-matrix-scale.md`
6
7
 
@@ -8,34 +9,63 @@
8
9
 
9
10
  | Field | Implemented | UI impact |
10
11
  |-------|-------------|-----------|
11
- | `statementRows[]`, `scaleColumns[]` | ✅ | Grid / carousel / dropdown data |
12
- | `displayStyle` | ✅ | Grid vs vertical list routing |
12
+ | `statementRows[]`, `scaleColumns[]` | ✅ | Grid data |
13
+ | `displayStyle` | ✅ | **numbered / graphics / standard** — NOT generic radio grid |
13
14
  | `gridLayout` | ✅ | carousel vs standard vs dropdown |
14
- | `hasNotApplicableOption` | ✅ | N/A column or checkbox |
15
- | `reverseScaleOrder` | ✅ | Flip emoji/star/number index |
16
15
  | `scaleAnchorLabels[]` | ✅ | Anchor row above columns |
17
- | `minLabel` / `maxLabel` | ✅ | Rendered in Question.tsx above scale |
18
- | `scaleColumns[].accentColor` | ✅ | NPS-style badge colors when numbered |
16
+ | `minLabel` / `maxLabel` | ✅ | Rendered in `Question.tsx` above scale |
17
+ | `hasNotApplicableOption` | ✅ | N/A column |
18
+ | `reverseScaleOrder` | ✅ | Flip column index |
19
19
 
20
- ## Config Decision Tree
20
+ ## Statement & label visibility (CRITICAL)
21
21
 
22
- Same as `CSAT_MATRIX` — reuse `CsatMatrixScale`. See [04-csat.md](04-csat.md) for emoji/star/carousel/N/A/graphics recipes.
22
+ Same rules as [`04-csat.md`](04-csat.md):
23
23
 
24
- ## Carousel / N/A / scale labels
24
+ 1. **180px row column** `statementRows[].statementText` via `dangerouslySetInnerHTML`
25
+ 2. **Header row** — `scaleAnchorLabels` OR `scaleColumns[].label` (never both missing)
26
+ 3. **Page 14 numbered fixture** — 10 column headers "1"–"10" above radio/badge row
25
27
 
26
- Follow CSAT Implementation Recipes in [04-csat.md](04-csat.md): pink carousel dots, N/A mutex, `tickLabels` from `scaleColumns[].label` on graphics sliders.
28
+ ## displayStyle branches (do NOT collapse to generic radios)
27
29
 
28
- ## Answer Mutation
30
+ | displayStyle | UI | Page fixture |
31
+ |--------------|-----|--------------|
32
+ | `numbered` | 40×40 magenta badges showing `colIdx+1`; tooltip = `col.label` | Page 14 Q8 |
33
+ | `graphics` | One `CustomSliderTrack` per row; `min=0`, `max=cols-1`, `tickLabels=scaleColumns.map(c=>c.label)` | Page 11 |
34
+ | `standard` | Custom 18px radio circles, `#e20074` accent | — |
35
+ | `emoji` / `star` | Same as CSAT — see [`04-csat.md`](04-csat.md) | — |
36
+
37
+ **graphics mode is NOT `SliderMatrixScale`** — it uses `CsatMatrixScale` with `CustomSliderTrack` per statement row.
38
+
39
+ ## minLabel / maxLabel (Page 11)
29
40
 
30
- Store **`column.id`** via `matrixColumnStoredValue(col)` see `02-reference/value-derivation.md`.
41
+ `Question.tsx` template renders these above `CsatMatrixScale` when set on the question.
42
+
43
+ ## Answer Mutation
31
44
 
32
45
  ```typescript
33
- onSelect(MatrixRowAnswers)
34
- onAction({ type: 'CHANGE', payload: { questionId, answerValue: MatrixRowAnswers } })
46
+ onSelect(MatrixRowAnswers) // keys = statementRows[].id, values = scaleColumns[].id
47
+ matrixColumnStoredValue(col) // returns col.id or null for N/A
35
48
  ```
36
49
 
37
50
  ## Agent Checklist
38
51
 
39
- - [ ] Reuse CsatMatrixScale with `RATING_MATRIX` type
40
- - [ ] Apply accentColor on numbered badges when present
41
- - [ ] minLabel / maxLabel above grid when set
52
+ - [ ] Reuse copied `CsatMatrixScale` — do not create separate RatingMatrixScale
53
+ - [ ] Statement text visible in 180px column
54
+ - [ ] Column headers or anchor labels visible
55
+ - [ ] `displayStyle: numbered` uses badges, not native radios
56
+ - [ ] `displayStyle: graphics` uses CustomSliderTrack per row
57
+
58
+ ## Wizard customization (Phase 5 — required)
59
+
60
+ > Reuses `CsatMatrixScale` patterns — see [`wizard-question-type-styling.md`](../00-integration/wizard-question-type-styling.md) § RATING_MATRIX.
61
+
62
+ | Format | Key tokens |
63
+ |--------|------------|
64
+ | radio | Horizontal grid + `matrixRadioRingStyle` |
65
+ | star / emoji / numbered | Same as CSAT formats |
66
+ | graphics / slider | `CustomSliderTrack` thumb ring + annulus + dot |
67
+
68
+ ### Wizard checklist
69
+
70
+ - [ ] Radio format is horizontal grid (not vertical list)
71
+ - [ ] `csatColumnLabelStyle()` on column headers