@explorer02/cfm-survey-sdk 0.2.2 → 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 (113) 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 +62 -17
  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/analytics-events-catalog.md +54 -0
  12. package/templates/docs/00-integration/apply-ui-config.md +365 -0
  13. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  14. package/templates/docs/00-integration/client-integration-guide.md +10 -8
  15. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  16. package/templates/docs/00-integration/component-checklist.md +90 -54
  17. package/templates/docs/00-integration/constraints.md +13 -5
  18. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  19. package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
  20. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  21. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  22. package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
  23. package/templates/docs/00-integration/progress.md +4 -2
  24. package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
  25. package/templates/docs/00-integration/setup.md +15 -3
  26. package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
  27. package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
  28. package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
  29. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  30. package/templates/docs/00-integration/wizard-api.md +134 -0
  31. package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
  32. package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
  33. package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
  34. package/templates/docs/01-components/01-survey-page.md +18 -8
  35. package/templates/docs/01-components/02-question.md +2 -2
  36. package/templates/docs/01-components/03-rating-scale.md +12 -18
  37. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  38. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -38
  39. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  40. package/templates/docs/01-components/08-file-upload-scale.md +15 -52
  41. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  42. package/templates/docs/01-components/10-header-footer.md +23 -4
  43. package/templates/docs/01-components/11-progress-bar.md +14 -6
  44. package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
  45. package/templates/docs/01-components/17-heatmap-scale.md +4 -4
  46. package/templates/docs/01-components/18-rank-order-scale.md +24 -31
  47. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  48. package/templates/docs/01-components/README.md +2 -2
  49. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  50. package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
  51. package/templates/docs/02-reference/question-types/README.md +11 -1
  52. package/templates/docs/02-reference/routing-table.md +1 -0
  53. package/templates/docs/02-reference/value-derivation.md +1 -1
  54. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  55. package/templates/docs/03-ui-specs/01-rating.md +28 -2
  56. package/templates/docs/03-ui-specs/02-radio.md +94 -24
  57. package/templates/docs/03-ui-specs/03-text.md +12 -1
  58. package/templates/docs/03-ui-specs/04-csat.md +90 -26
  59. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  60. package/templates/docs/03-ui-specs/07-matrix-cfm.md +74 -35
  61. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +50 -16
  62. package/templates/docs/03-ui-specs/09-slider-matrix.md +70 -26
  63. package/templates/docs/03-ui-specs/10-file-upload.md +52 -20
  64. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  65. package/templates/docs/03-ui-specs/12-survey-chrome.md +92 -5
  66. package/templates/docs/03-ui-specs/13-heatmap.md +21 -3
  67. package/templates/docs/03-ui-specs/14-rank-order.md +108 -28
  68. package/templates/docs/03-ui-specs/README.md +11 -7
  69. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  70. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  71. package/templates/docs/MANIFEST.json +187 -6
  72. package/templates/docs/index.md +116 -116
  73. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  74. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  75. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  76. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  77. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  78. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  79. package/templates/docs/templates/Question.tsx +3 -0
  80. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  81. package/templates/docs/templates/RatingScale.tsx +129 -0
  82. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  83. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  84. package/templates/docs/templates/customFieldValues.ts +36 -0
  85. package/templates/docs/templates/implementation_plan.md +75 -8
  86. package/templates/docs/templates/labelStyles.ts +33 -0
  87. package/templates/docs/templates/selectionStyles.ts +100 -0
  88. package/templates/docs/templates/survey-inventory.schema.json +28 -23
  89. package/templates/docs/templates/surveyUiIcons.tsx +63 -0
  90. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  91. package/templates/docs/templates/verify-agent-build.sh +151 -0
  92. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  93. package/templates/preview-harness/previewPages.js +108 -0
  94. package/templates/preview-harness/previewPages.ts +298 -0
  95. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  96. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  97. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  98. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  99. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  100. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  101. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  102. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  103. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  104. package/templates/previewBridge.ts +153 -0
  105. package/templates/survey-theme.css +295 -0
  106. package/templates/survey-ui-config.schema.json +213 -0
  107. package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
  108. package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
  109. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  110. package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
  111. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  112. package/templates/wizard-dist/index.html +20 -0
  113. package/templates/wizard.html +13 -1129
@@ -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,11 +10,56 @@
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) |
18
+
19
+ ## Statement & label visibility (CRITICAL)
20
+
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>
34
+ ```
35
+
36
+ ### 2. Column labels — two modes (mutually exclusive header row)
37
+
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` |
42
+
43
+ **When `scaleAnchorLabels` is set**, still render anchor row even if column text labels are long (emoji mode Page 9 fixture):
44
+
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
+ ```
51
+
52
+ **When no anchor labels**, render column headers:
53
+
54
+ ```tsx
55
+ {scaleColumns.map(col => (
56
+ <span dangerouslySetInnerHTML={{ __html: col.label }} />
57
+ ))}
58
+ ```
59
+
60
+ ### 3. N/A column header
61
+
62
+ When `hasNotApplicableOption`, reserve **56px** right column with "N/A" header text.
17
63
 
18
64
  ## Config Decision Tree
19
65
 
@@ -21,33 +67,32 @@
21
67
  CSAT_MATRIX
22
68
  ├── gridLayout === 'carousel' → CsatMatrixCarousel
23
69
  ├── displayStyle in (dropdown, selectbox) → CsatMatrixVerticalList
24
- │ ├── dropdown → MatrixDropdown placeholder "Select..."
25
- │ ├── selectbox → full-width cards #fdf2f8 selected
26
- │ └── else → card radio list
27
70
  └── 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)
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
32
76
  ```
33
77
 
34
- ## Grid Structure
78
+ ## Emoji + anchor labels (Page 9 fixture)
79
+
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
35
83
 
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
84
+ ## Star mode (Page 14 fixture)
40
85
 
41
- ## Carousel
86
+ - `CsatStarIcons` from `surveyUiIcons.tsx`
87
+ - Unselected opacity 0.5; selected filled star `#e20074`
42
88
 
43
- - Pink active dot expands: `20px × 8px` vs `8px` inactive
44
- - Column labels below cells
45
- - Emoji tooltip via hover state
46
- - Graphics mode: slider maps column index
89
+ ## Graphics / numbered (RATING_MATRIX reuse)
90
+
91
+ See [`08-matrix-csat-rating.md`](08-matrix-csat-rating.md) same `CsatMatrixScale` component.
47
92
 
48
93
  ## Answer Mutation
49
94
 
50
- Store **`column.id`** via `matrixColumnStoredValue(col)` — see `02-reference/value-derivation.md`.
95
+ Store **`column.id`** via `matrixColumnStoredValue(col)`:
51
96
 
52
97
  ```typescript
53
98
  onSelect(MatrixRowAnswers)
@@ -56,7 +101,26 @@ onAction({ type: 'CHANGE', payload: { questionId, answerValue: MatrixRowAnswers
56
101
 
57
102
  ## Agent Checklist
58
103
 
59
- - [ ] All displayStyle × gridLayout branches
60
- - [ ] reverseScaleOrder on emoji/star/numbered
61
- - [ ] Carousel pink dot indicators
62
- - [ ] 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,52 +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 |
31
+
32
+ Always use `dangerouslySetInnerHTML` for HTML statement text.
33
33
 
34
- ## Interactive Behaviors
34
+ ### Bipolar right column
35
35
 
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 |
36
+ When `statementLayout === 'bipolar'`, render **`oppositeStatementText`** in 25% right column (unless transposed):
42
37
 
43
- ## Grid Layout
38
+ ```tsx
39
+ <span dangerouslySetInnerHTML={{ __html: row.oppositeStatementText ?? '' }} />
40
+ ```
41
+
42
+ ### Column / anchor labels
44
43
 
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
44
+ Header renders when `scaleAnchorLabels` OR `showColumnHeaders`:
48
45
 
49
- ## Carousel
46
+ **Bipolar + anchor labels (Page 7 fixture)** — `showColumnHeaders: false`:
50
47
 
51
- - Blue active dot: `#2563eb` (Likert only)
52
- - Card: white, border, shadow, centered row text
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
+ ```
53
52
 
54
- ## Answer Mutation
53
+ Anchor positioning:
54
+
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
+ ```
55
61
 
56
- Store **`column.id`** via `matrixColumnStoredValue(col)`see `02-reference/value-derivation.md`.
62
+ **Transposed + column headers (Page 10 fixture)**`transposeTable: true`, `showColumnHeaders: true`:
57
63
 
58
64
  ```typescript
59
- onSelect(MatrixRowAnswers) // Record<statementRows[].id, value | value[] | null>
60
- 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
61
84
  ```
62
85
 
63
86
  ## Agent Checklist
64
87
 
65
- - [ ] All three gridLayout branches
66
- - [ ] transposeTable + repeatColumnHeaders + showColumnHeaders
67
- - [ ] Bipolar 25/50/25 layout with oppositeStatementText
68
- - [ ] Single-select deselect on re-click
69
- - [ ] 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,30 +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` grid — reuse `CsatMatrixScale` component. See [04-csat.md](04-csat.md) decision tree with `type: 'RATING_MATRIX'`.
22
+ Same rules as [`04-csat.md`](04-csat.md):
23
23
 
24
- ## Answer Mutation
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
27
+
28
+ ## displayStyle branches (do NOT collapse to generic radios)
29
+
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)
40
+
41
+ `Question.tsx` template renders these above `CsatMatrixScale` when set on the question.
25
42
 
26
- Store **`column.id`** via `matrixColumnStoredValue(col)` — see `02-reference/value-derivation.md`.
43
+ ## Answer Mutation
27
44
 
28
45
  ```typescript
29
- onSelect(MatrixRowAnswers)
30
- 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
31
48
  ```
32
49
 
33
50
  ## Agent Checklist
34
51
 
35
- - [ ] Reuse CsatMatrixScale with `RATING_MATRIX` type
36
- - [ ] Apply accentColor on numbered badges when present
37
- - [ ] 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
@@ -1,61 +1,105 @@
1
1
  # UI Spec: Slider Matrix (`type: 'SLIDER_MATRIX'`)
2
2
 
3
- > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/SliderMatrixScale.tsx`
3
+ > **COPY TEMPLATE FIRST:** [`templates/SliderMatrixScale.tsx`](../templates/SliderMatrixScale.tsx) + [`CustomSliderTrack.tsx`](../templates/CustomSliderTrack.tsx) + [`surveyUiScaleUtils.ts`](../templates/surveyUiScaleUtils.ts)
4
+ >
5
+ > **Anti-pattern:** Do NOT build custom range overlay sliders. Lone pink circle with no track = broken implementation.
6
+ >
4
7
  > **SDK type:** `SliderMatrixQuestion` — `fetchSurvey/types/sliderMatrix.ts`
5
8
  > **Component blueprint:** `01-components/07-slider-matrix-scale.md`
6
- > **Shared:** [`shared/custom-slider-track.md`](shared/custom-slider-track.md)
7
9
 
8
10
  ## Config Inventory
9
11
 
10
12
  | Field | Implemented | UI impact |
11
13
  |-------|-------------|-----------|
12
- | `rows[]` (min/max/step) | ✅ | Per-row slider range |
13
- | `labels[]` | ✅ | Header anchor labels |
14
- | `tickValues[]` | ✅ | Colored badges above track |
15
- | `ticks` | ✅ | Discrete marks + htmlStep |
16
- | `sliderType: 'graphics'` | ✅ | Plain text tick labels; emoji thumb |
14
+ | `statementRows[]` (min/max/step) | ✅ | Per-row slider + **25% statement column** |
15
+ | `scaleAnchorLabels?` | ✅ | Text labels above tick badge row |
16
+ | `ticks.count` + `ticks.marks[]` | ✅ | Colored header badges (`tickPosition`, `tickLabel`) |
17
+ | `sliderStyle: 'graphics'` | ✅ | Plain text tick labels; emoji thumb |
17
18
  | `enableInputBox` | ✅ | 80px input + stepper buttons |
18
19
  | `displayValues` | ✅ | Passed to CustomSliderTrack |
19
- | `enableNotApplicable` | ✅ | Checkbox; value `'N/A'` string |
20
+ | `hasNotApplicableOption` | ✅ | Checkbox; answer value **`'N/A'`** string |
20
21
 
21
22
  ## Layout Anatomy
22
23
 
23
24
  ```
24
- [header: labels[] + tickValues badges OR computed marks]
25
+ [header: scaleAnchorLabels? + colored tick badges from ticks.marks]
25
26
  ┌─ row band (#f3f4f6, rounded-xl) ─────────────────────────────┐
26
- │ 25% statement │ CustomSliderTrack │ 25% rightText? │ input? │ N/A? │
27
+ │ 25% statementText │ CustomSliderTrack │ 80px input? │ N/A? │
27
28
  └───────────────────────────────────────────────────────────────┘
28
29
  ```
29
30
 
30
- ## Header Tick Positioning
31
+ ## Statement text (CRITICAL)
32
+
33
+ Every row MUST show `statementRows[].statementText` in the left 25% column:
34
+
35
+ ```tsx
36
+ <div style={{ flex: '0 0 25%', paddingRight: '16px' }}>
37
+ {row.statementText && row.statementText !== 'Question' && (
38
+ <span dangerouslySetInnerHTML={{ __html: row.statementText }} />
39
+ )}
40
+ </div>
41
+ ```
42
+
43
+ Agents that hide statement text or merge it into the question title break multi-row sliders.
44
+
45
+ ## Header tick badges
31
46
 
32
47
  ```typescript
33
- percentage = ((tv.value - min) / (max - min)) * 100
34
- sliceWidth = tickValues.length > 1 ? 100 / (tickValues.length - 1) : 100
35
- // Colored badge: tv.color bg, white text, rounded 4px
36
- // graphics: plain text label instead of colored badge
48
+ tickValues = ticks.marks.map((mark, i) => ({
49
+ value: mark.tickPosition,
50
+ label: mark.tickLabel,
51
+ color: tickColorFromIndex(i, ticks.count),
52
+ }));
53
+ percentage = ((tv.value - min) / (max - min)) * 100;
54
+ htmlStep = ticks.count > 1 ? (max - min) / (ticks.count - 1) : row.step;
37
55
  ```
38
56
 
39
- ## N/A Behavior
57
+ ## CustomSliderTrack per row
40
58
 
41
- - Checked → `rowVal === 'N/A'`; slider disabled, input grayed
42
- - Unchecked → reset to `defaultValue ?? min`
59
+ ```typescript
60
+ <CustomSliderTrack
61
+ min={row.min}
62
+ max={row.max}
63
+ htmlStep={htmlStep}
64
+ value={rowVal as number}
65
+ disabled={rowVal === 'N/A'}
66
+ hasSelectedValue={typeof rowVal === 'number'}
67
+ displayValues={displayValues}
68
+ sliderType={question.sliderStyle === 'graphics' ? 'graphics' : undefined}
69
+ ticks={ticks.count}
70
+ onChange={v => updateRow(row.id, v)}
71
+ />
72
+ ```
43
73
 
44
- ## Input Box
74
+ ## N/A Behavior
45
75
 
46
- - Clamp to row.min / row.max on change
47
- - Steppers use `htmlStep` increment/decrement
76
+ - Checked `rowVal === 'N/A'`; slider disabled
77
+ - Unchecked reset to `defaultValue ?? min`
48
78
 
49
79
  ## Answer Mutation
50
80
 
51
81
  ```typescript
52
- onSelect(Record<string, number | null | 'N/A'>)
82
+ onSelect(Record<string, number | 'N/A'>)
83
+ onAction({ type: 'CHANGE', payload: { questionId, answerValue: MatrixRowAnswers } })
53
84
  ```
54
85
 
55
86
  ## Agent Checklist
56
87
 
57
- - [ ] Shared header from first row min/max/step
58
- - [ ] Gray row bands with 25% statement columns
88
+ - [ ] Copied portable templates did not build slider from scratch
89
+ - [ ] Shared header with colored 0–10 badges (even single-row questions)
90
+ - [ ] Gray row bands with **visible** statement text in 25% column
59
91
  - [ ] CustomSliderTrack per row with correct htmlStep
60
- - [ ] tickValues colored badges (non-graphics)
61
- - [ ] N/A mutex disables slider + input
92
+ - [ ] N/A uses `'N/A'` string, not null
93
+
94
+ ## Wizard customization (Phase 5 — required)
95
+
96
+ | Control | CSS vars | Apply in |
97
+ |---------|----------|----------|
98
+ | Tick label items | `--cfm-slider-tick-label-color`, `--cfm-slider-tick-label-bg` | Header badge row — `sliderTickLabelStyle()` |
99
+ | Row band | `--cfm-slider-row-bg` | Gray statement band |
100
+ | Thumb selection | `--cfm-matrix-selected`, `--cfm-input-focus-ring` | `CustomSliderTrack` thumb ring + annulus + dot |
101
+
102
+ ### Wizard checklist
103
+
104
+ - [ ] Colored tick badges use `--cfm-number-*` when traffic-light mode applies
105
+ - [ ] `labelStyles.ts` imported for tick label row
@@ -1,18 +1,18 @@
1
1
  # UI Spec: File Upload (`type: 'FILE_UPLOAD'`)
2
2
 
3
- > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/FileUploadScale.tsx`
4
3
  > **SDK type:** `FileUploadQuestion` — `fetchSurvey/types/fileUpload.ts`
5
4
  > **Component blueprint:** `01-components/08-file-upload-scale.md`
5
+ > **Upload API:** [`00-integration/file-upload-aws.md`](../00-integration/file-upload-aws.md)
6
6
 
7
7
  ## Config Inventory
8
8
 
9
- | Field | Implemented | UI impact |
10
- |-------|-------------|-----------|
11
- | `uploadMessage` | ✅ | HTML in dropzone (bold 16px) |
12
- | `supportedFileFormats` | ✅ | Validation + accept attribute |
13
- | `maxFileCount` | ✅ | Total file limit |
14
- | `fileSizeLimit` | ✅ | MB limit |
15
- | `fileSizeLimitType` | ✅ | `PER_FILE` or `IN_TOTAL` |
9
+ | Field | UI impact |
10
+ |-------|-----------|
11
+ | `uploadMessage` | HTML in dropzone (bold 16px) |
12
+ | `supportedFileFormats` | Validation + accept attribute |
13
+ | `maxFileCount` | Total file limit |
14
+ | `fileSizeLimit` | MB limit |
15
+ | `fileSizeLimitType` | `PER_FILE` or `IN_TOTAL` |
16
16
 
17
17
  ## Layout Anatomy
18
18
 
@@ -20,7 +20,7 @@
20
20
  [uploadMessage HTML in dashed dropzone]
21
21
  File limit: N; File size limit: XMB/file|in total; Supports: ...
22
22
  [local error banner — red if validation fails]
23
- [file list cards: icon, name, size, remove ×]
23
+ [file list cards: icon, name, size, uploading…, remove ×]
24
24
  ```
25
25
 
26
26
  ## Interactive States
@@ -31,24 +31,56 @@
31
31
  | Hover | `#e20074` | `#fdf2f8` |
32
32
  | Drag active | `#e20074` | `#fdf2f8` |
33
33
 
34
- ## Local Validation (before SDK)
34
+ ## Implementation Recipe
35
35
 
36
- 1. Max file count exceeded
37
- 2. Unsupported extension
38
- 3. PER_FILE size exceeded
39
- 4. IN_TOTAL cumulative size exceeded
36
+ ### Phase 1 Local pick + validate
40
37
 
41
- Error banner: `#fee2e2` bg, `#b91c1c` text.
38
+ 1. Hidden `<input type="file" multiple={maxFileCount > 1}>`
39
+ 2. Drag enter/over/leave/drop on dropzone
40
+ 3. Reject: wrong extension, count exceeded, size exceeded
41
+
42
+ ### Phase 2 — Upload (required for submit)
43
+
44
+ ```
45
+ User picks File
46
+ → POST {uploadApi}/get-upload-url { fileName, contentType, questionId }
47
+ ← { uploadUrl, fileId, mediaUrl }
48
+ → PUT uploadUrl (binary body, file Content-Type)
49
+ → onSelect([...existing, { id: fileId, mediaUrl, name, mimeType, size }])
50
+ → onAction(CHANGE)
51
+ ```
52
+
53
+ See [`file-upload-aws.md`](../00-integration/file-upload-aws.md).
54
+
55
+ ### Phase 3 — Remove
56
+
57
+ Remove from list → `onSelect(undefined)` when empty, else updated `UploadedFileAnswer[]`.
42
58
 
43
59
  ## Answer Mutation
44
60
 
45
61
  ```typescript
46
- onSelect(File[] | undefined) // undefined when all removed
62
+ // After upload completes NOT raw File[]
63
+ onSelect: (value: UploadedFileAnswer[] | undefined) => void
64
+ onAction({ type: 'CHANGE', payload: { questionId, answerValue: UploadedFileAnswer[] } })
47
65
  ```
48
66
 
67
+ **Non-submittable:** storing `File[]` only — SDK submit drops attachments.
68
+
49
69
  ## Agent Checklist
50
70
 
51
- - [ ] Hidden `<input type="file">` triggered by dropzone click
52
- - [ ] Drag enter/over/leave/drop handlers
53
- - [ ] Dynamic constraints line in dropzone
54
- - [ ] File list with remove button (hover red)
71
+ - [ ] Local validation before upload
72
+ - [ ] Presigned upload wired; progress/spinner per file
73
+ - [ ] Final answer shape: `UploadedFileAnswer[]` with `id` + `mediaUrl`
74
+ - [ ] Remove clears answer or passes updated array
75
+ - [ ] Matrix row: [`question-type-sdk-matrix.md#file_upload`](../00-integration/question-type-sdk-matrix.md#file_upload)
76
+
77
+ ## Wizard customization (Phase 5 — required)
78
+
79
+ | Control | CSS vars | Apply in |
80
+ |---------|----------|----------|
81
+ | Dropzone border | `--cfm-upload-border-color` | Dashed border on upload area |
82
+ | Focus / drag-over | `--cfm-input-focus-ring` | Active dropzone outline |
83
+
84
+ ### Wizard checklist
85
+
86
+ - [ ] Dropzone border uses `var(--cfm-upload-border-color)` not hardcoded hex
@@ -47,3 +47,7 @@ Container-level media (survey chrome) is separate — see `03-ui-specs/12-survey
47
47
 
48
48
  - [ ] No CHANGE action — display-only
49
49
  - [ ] Matrix row: [`question-type-sdk-matrix.md#text_and_media`](../00-integration/question-type-sdk-matrix.md#text_and_media)
50
+
51
+ ## Wizard customization (Phase 5 — required)
52
+
53
+ Display-only — no selection tokens. Media caption may use `--cfm-question-title-color` for consistency.