@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.
- package/dist/cli/index.js +140 -33
- package/dist/cli/index.mjs +140 -33
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +8 -1
- package/postinstall.js +8 -5
- package/templates/AGENT.md +62 -17
- package/templates/deploy-wizard-to-s3.sh +52 -0
- package/templates/docs/00-integration/agent-execution-flow.md +281 -0
- package/templates/docs/00-integration/agent-operating-contract.md +260 -0
- package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
- package/templates/docs/00-integration/apply-ui-config.md +365 -0
- package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
- package/templates/docs/00-integration/client-integration-guide.md +10 -8
- package/templates/docs/00-integration/client-lib-folder.md +115 -0
- package/templates/docs/00-integration/component-checklist.md +90 -54
- package/templates/docs/00-integration/constraints.md +13 -5
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
- package/templates/docs/00-integration/file-upload-aws.md +116 -0
- package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
- package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
- package/templates/docs/00-integration/progress.md +4 -2
- package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
- package/templates/docs/00-integration/setup.md +15 -3
- package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
- package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
- package/templates/docs/00-integration/useSurveySDK.md +35 -1
- package/templates/docs/00-integration/wizard-api.md +134 -0
- package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
- package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
- package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
- package/templates/docs/01-components/01-survey-page.md +18 -8
- package/templates/docs/01-components/02-question.md +2 -2
- package/templates/docs/01-components/03-rating-scale.md +12 -18
- package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
- package/templates/docs/01-components/06-likert-matrix-scale.md +19 -38
- package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
- package/templates/docs/01-components/08-file-upload-scale.md +15 -52
- package/templates/docs/01-components/09-custom-slider-track.md +15 -25
- package/templates/docs/01-components/10-header-footer.md +23 -4
- package/templates/docs/01-components/11-progress-bar.md +14 -6
- package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
- package/templates/docs/01-components/17-heatmap-scale.md +4 -4
- package/templates/docs/01-components/18-rank-order-scale.md +24 -31
- package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
- package/templates/docs/01-components/README.md +2 -2
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
- package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
- package/templates/docs/02-reference/question-types/README.md +11 -1
- package/templates/docs/02-reference/routing-table.md +1 -0
- package/templates/docs/02-reference/value-derivation.md +1 -1
- package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
- package/templates/docs/03-ui-specs/01-rating.md +28 -2
- package/templates/docs/03-ui-specs/02-radio.md +94 -24
- package/templates/docs/03-ui-specs/03-text.md +12 -1
- package/templates/docs/03-ui-specs/04-csat.md +90 -26
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +74 -35
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +50 -16
- package/templates/docs/03-ui-specs/09-slider-matrix.md +70 -26
- package/templates/docs/03-ui-specs/10-file-upload.md +52 -20
- package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
- package/templates/docs/03-ui-specs/12-survey-chrome.md +92 -5
- package/templates/docs/03-ui-specs/13-heatmap.md +21 -3
- package/templates/docs/03-ui-specs/14-rank-order.md +108 -28
- package/templates/docs/03-ui-specs/README.md +11 -7
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
- package/templates/docs/MANIFEST.json +187 -6
- package/templates/docs/index.md +116 -116
- package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
- package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
- package/templates/docs/templates/FileUploadScale.tsx +262 -0
- package/templates/docs/templates/HeatmapScale.tsx +114 -0
- package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
- package/templates/docs/templates/MatrixDropdown.tsx +216 -0
- package/templates/docs/templates/Question.tsx +3 -0
- package/templates/docs/templates/RankOrderScale.tsx +353 -0
- package/templates/docs/templates/RatingScale.tsx +129 -0
- package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
- package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
- package/templates/docs/templates/customFieldValues.ts +36 -0
- package/templates/docs/templates/implementation_plan.md +75 -8
- package/templates/docs/templates/labelStyles.ts +33 -0
- package/templates/docs/templates/selectionStyles.ts +100 -0
- package/templates/docs/templates/survey-inventory.schema.json +28 -23
- package/templates/docs/templates/surveyUiIcons.tsx +63 -0
- package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
- package/templates/docs/templates/verify-agent-build.sh +151 -0
- package/templates/preview-harness/preview-bridge.inline.js +201 -0
- package/templates/preview-harness/previewPages.js +108 -0
- package/templates/preview-harness/previewPages.ts +298 -0
- package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
- package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
- package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
- package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
- package/templates/preview-harness/vite-app/src/globals.css +16 -0
- package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
- package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
- package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
- package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
- package/templates/previewBridge.ts +153 -0
- package/templates/survey-theme.css +295 -0
- package/templates/survey-ui-config.schema.json +213 -0
- package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
- package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
- package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
- package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
- package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
- package/templates/wizard-dist/index.html +20 -0
- package/templates/wizard.html +13 -1129
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Rank Order Architectural Blueprint
|
|
2
2
|
|
|
3
3
|
> **UI spec (authoritative):** `03-ui-specs/14-rank-order.md`
|
|
4
|
-
> **
|
|
4
|
+
> **Portable template:** copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx)
|
|
5
5
|
> **Target Component:** `RankOrderScale`
|
|
6
6
|
> **Handles:** `type: 'RANK_ORDER'`
|
|
7
7
|
|
|
8
8
|
## Core Responsibility
|
|
9
9
|
|
|
10
|
-
Render rankable options in
|
|
10
|
+
Render rankable options in **dropdown** or **drag-and-drop** mode, three option display variants, bubble ranks via `onSelect`.
|
|
11
11
|
|
|
12
12
|
## Props Contract
|
|
13
13
|
|
|
@@ -21,9 +21,9 @@ type RankOrderScaleProps = {
|
|
|
21
21
|
|
|
22
22
|
## State Management
|
|
23
23
|
|
|
24
|
-
- `selectedValue` from SDK is source of truth —
|
|
25
|
-
- Dropdown
|
|
26
|
-
- Drag-and-drop
|
|
24
|
+
- `selectedValue` from SDK is source of truth — no duplicate local answer state
|
|
25
|
+
- Dropdown: `assignRankWithoutDuplicates` on rank change
|
|
26
|
+
- Drag-and-drop: `buildRankOrderFromOrderedOptionIds` after reorder; `getOrderedOptionIdsFromRanks` for display order
|
|
27
27
|
|
|
28
28
|
## Layout Orchestrator
|
|
29
29
|
|
|
@@ -36,20 +36,9 @@ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderS
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## shuffleOptions
|
|
40
40
|
|
|
41
|
-
-
|
|
42
|
-
- Left: rank `<select>` with `-` (unassigned) and `1..N`
|
|
43
|
-
- Selecting rank `R` clears `R` from any other option (SDK helper)
|
|
44
|
-
- Option content depends on `question.optionDisplay`
|
|
45
|
-
- **Image URLs:** prefer `option.imageUrl` over `option.previewImageUrl` — preview `*_p.*` URLs often return 403 on CloudFront
|
|
46
|
-
|
|
47
|
-
## Drag-and-Drop Mode
|
|
48
|
-
|
|
49
|
-
- Use `@dnd-kit/core` + `@dnd-kit/sortable` for smooth reorder
|
|
50
|
-
- List order maps to ranks: position 0 → rank 1
|
|
51
|
-
- Show rank badge on each row
|
|
52
|
-
- Provide Up/Down buttons for keyboard accessibility
|
|
41
|
+
When API `SHUFFLE_ORDER` is true, SDK shuffles options at map time. **Do not re-shuffle in the client** — logic and option ids must stay aligned with fetched order.
|
|
53
42
|
|
|
54
43
|
## Wiring in Question.tsx
|
|
55
44
|
|
|
@@ -57,6 +46,7 @@ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderS
|
|
|
57
46
|
import {
|
|
58
47
|
QUESTION_TYPE,
|
|
59
48
|
getVisibleRankOrderOptionsForAnswers,
|
|
49
|
+
normalizeRankOrderAnswers,
|
|
60
50
|
} from '@explorer02/cfm-survey-sdk';
|
|
61
51
|
|
|
62
52
|
const rankOrderQuestion = useMemo(() => {
|
|
@@ -64,10 +54,7 @@ const rankOrderQuestion = useMemo(() => {
|
|
|
64
54
|
return {
|
|
65
55
|
...question,
|
|
66
56
|
options: getVisibleRankOrderOptionsForAnswers(
|
|
67
|
-
question,
|
|
68
|
-
allAnswers,
|
|
69
|
-
allQuestions,
|
|
70
|
-
customFieldValues
|
|
57
|
+
question, allAnswers, allQuestions, customFieldValues
|
|
71
58
|
),
|
|
72
59
|
};
|
|
73
60
|
}, [question, allAnswers, allQuestions, customFieldValues]);
|
|
@@ -75,7 +62,11 @@ const rankOrderQuestion = useMemo(() => {
|
|
|
75
62
|
{question.type === QUESTION_TYPE.RANK_ORDER && rankOrderQuestion && (
|
|
76
63
|
<RankOrderScale
|
|
77
64
|
question={rankOrderQuestion}
|
|
78
|
-
selectedValue={
|
|
65
|
+
selectedValue={normalizeRankOrderAnswers(
|
|
66
|
+
typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue)
|
|
67
|
+
? selectedValue
|
|
68
|
+
: undefined
|
|
69
|
+
)}
|
|
79
70
|
onSelect={onSelect}
|
|
80
71
|
/>
|
|
81
72
|
)}
|
|
@@ -83,18 +74,20 @@ const rankOrderQuestion = useMemo(() => {
|
|
|
83
74
|
|
|
84
75
|
## Dependencies
|
|
85
76
|
|
|
86
|
-
|
|
77
|
+
```bash
|
|
78
|
+
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Next.js SSR
|
|
87
82
|
|
|
88
|
-
-
|
|
89
|
-
- `@dnd-kit/sortable`
|
|
90
|
-
- `@dnd-kit/utilities`
|
|
83
|
+
Template uses `'use client'`. For drag-and-drop, optional `dynamic(..., { ssr: false })` in Question.tsx if hydration warnings occur.
|
|
91
84
|
|
|
92
85
|
## SDK Integration Checklist (agent)
|
|
93
86
|
|
|
87
|
+
- [ ] Copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx)
|
|
94
88
|
- [ ] Type literal: `QUESTION_TYPE.RANK_ORDER`
|
|
95
|
-
- [ ] Answer
|
|
96
|
-
- [ ]
|
|
97
|
-
- [ ]
|
|
98
|
-
- [ ] Rank utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `normalizeRankOrderAnswers`
|
|
89
|
+
- [ ] Answer: `RankOrderAnswers`
|
|
90
|
+
- [ ] Visibility: `getVisibleRankOrderOptionsForAnswers(..., customFieldValues?)`
|
|
91
|
+
- [ ] Utils: `assignRankWithoutDuplicates`, `buildRankOrderFromOrderedOptionIds`, `normalizeRankOrderAnswers`
|
|
99
92
|
- [ ] Prefer `option.imageUrl` over `previewImageUrl`
|
|
100
93
|
- [ ] Matrix row: [`question-type-sdk-matrix.md#rank_order`](../00-integration/question-type-sdk-matrix.md#rank_order)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Survey Sticky Chrome
|
|
2
|
+
|
|
3
|
+
> **UI spec (authoritative):** `03-ui-specs/12-survey-chrome.md`
|
|
4
|
+
> **Reference implementation:** `apps/client/src/components/SurveyStickyChrome.tsx`
|
|
5
|
+
> **Template:** [`templates/SurveyStickyChrome.tsx`](../templates/SurveyStickyChrome.tsx)
|
|
6
|
+
> **Component**: `SurveyStickyChrome`
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Wrap **Header + ProgressBar** in a single `sticky top-0` container so the progress bar stays visible when the respondent scrolls to answer questions lower on the page (especially the last question on a multi-question page).
|
|
11
|
+
|
|
12
|
+
## Props
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
type SurveyStickyChromeProps = {
|
|
16
|
+
progressPercentage: number; // from state.progressPercentage
|
|
17
|
+
};
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Implementation
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
export default function SurveyStickyChrome({ progressPercentage }: SurveyStickyChromeProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="sticky top-0 z-30 bg-white shadow-[0_2px_8px_rgba(0,0,0,0.06)]">
|
|
26
|
+
<Header embedded />
|
|
27
|
+
<div className="mx-auto w-full max-w-4xl px-4 pb-5 pt-4 sm:px-6 lg:px-8">
|
|
28
|
+
<ProgressBar progressPercentage={progressPercentage} />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Spacing (logo → progress bar)
|
|
36
|
+
|
|
37
|
+
Keep a visible gap between the logo block and the progress track — do **not** flush them together.
|
|
38
|
+
|
|
39
|
+
| Token | Tailwind | Purpose |
|
|
40
|
+
|-------|----------|---------|
|
|
41
|
+
| Top inset | `pt-4` | 16px gap below logo / header row |
|
|
42
|
+
| Bottom inset | `pb-5` | 20px gap above scrollable question content |
|
|
43
|
+
| Horizontal | `px-4 sm:px-6 lg:px-8` | Align track with `main` content column (`max-w-4xl`) |
|
|
44
|
+
|
|
45
|
+
## Header `embedded` prop
|
|
46
|
+
|
|
47
|
+
When `Header` is inside sticky chrome, pass `embedded` to avoid a double shadow:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<header className={`relative h-[120px] w-full bg-white ${embedded ? '' : 'z-10 shadow-[0_2px_8px_rgba(0,0,0,0.06)]'}`}>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use the standalone `Header` (no `embedded`) on intro, end, loading, and terminal pages.
|
|
54
|
+
|
|
55
|
+
## SurveyPage wiring (content phase only)
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<div className="flex min-h-screen flex-col bg-white">
|
|
59
|
+
<SurveyStickyChrome progressPercentage={state.progressPercentage} />
|
|
60
|
+
|
|
61
|
+
<main className="mx-auto w-full max-w-4xl flex-1 px-4 py-10 sm:px-6 lg:px-8">
|
|
62
|
+
<LanguageSelector ... />
|
|
63
|
+
{/* questions + nav */}
|
|
64
|
+
</main>
|
|
65
|
+
|
|
66
|
+
<Footer />
|
|
67
|
+
</div>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Do **not** render a separate `ProgressBar` inside `<main>` — it belongs in sticky chrome only.
|
|
71
|
+
|
|
72
|
+
## Agent checklist
|
|
73
|
+
|
|
74
|
+
- [ ] `SurveyStickyChrome` (or equivalent sticky wrapper) on `phase.kind === 'content'`
|
|
75
|
+
- [ ] `ProgressBar` bound to `state.progressPercentage`
|
|
76
|
+
- [ ] `sticky top-0 z-30 bg-white` on wrapper; single shadow on wrapper, not Header
|
|
77
|
+
- [ ] `pt-4` gap between logo and progress track; `pb-5` below track
|
|
78
|
+
- [ ] Progress track aligned to same `max-w-4xl` as question content
|
|
79
|
+
- [ ] Standalone `Header` on intro / end / loading / terminal branches
|
|
@@ -15,8 +15,7 @@ SurveyPage (phase router)
|
|
|
15
15
|
├── IntroPage / PausedPage / EndPage (special phases)
|
|
16
16
|
│
|
|
17
17
|
└── SurveyContent (phase.kind === 'content')
|
|
18
|
-
├── Header
|
|
19
|
-
├── ProgressBar
|
|
18
|
+
├── SurveyStickyChrome (sticky Header + ProgressBar)
|
|
20
19
|
├── LanguageSelector
|
|
21
20
|
├── Question × N (dispatcher)
|
|
22
21
|
├── Back / Next|Submit nav
|
|
@@ -62,6 +61,7 @@ Shared: `CustomSliderTrack`, `MatrixDropdown`
|
|
|
62
61
|
| `09-custom-slider-track.md` | `CustomSliderTrack` |
|
|
63
62
|
| `10-header-footer.md` | `Header` & `Footer` |
|
|
64
63
|
| `11-progress-bar.md` | `ProgressBar` |
|
|
64
|
+
| `19-survey-sticky-chrome.md` | `SurveyStickyChrome` (sticky Header + ProgressBar) |
|
|
65
65
|
| `12-language-selector.md` | `LanguageSelector` |
|
|
66
66
|
| `13-matrix-dropdown.md` | `MatrixDropdown` |
|
|
67
67
|
| `14-intro-page.md` | `IntroPage` |
|
|
@@ -2,50 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
> **Type**: `'SLIDER_MATRIX'` | **API Source**: `SLIDER_MATRIX`
|
|
4
4
|
> **Mapper**: `sliderMatrixMapper.ts` | **Component**: `SliderMatrixScale`
|
|
5
|
+
> **Portable template**: [`templates/SliderMatrixScale.tsx`](../../templates/SliderMatrixScale.tsx)
|
|
5
6
|
|
|
6
7
|
## TypeScript Type
|
|
7
8
|
|
|
8
9
|
```typescript
|
|
9
|
-
type SliderMatrixQuestion =
|
|
10
|
+
type SliderMatrixQuestion = {
|
|
10
11
|
type: 'SLIDER_MATRIX';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
id: string;
|
|
13
|
+
questionText: string;
|
|
14
|
+
questionDescription?: string;
|
|
15
|
+
required: boolean;
|
|
16
|
+
questionNumber?: number;
|
|
17
|
+
statementRows: SliderMatrixStatementRow[];
|
|
18
|
+
scaleAnchorLabels?: string[];
|
|
19
|
+
ticks: {
|
|
20
|
+
count: number;
|
|
21
|
+
marks: { tickPosition: number; tickLabel: string }[];
|
|
22
|
+
};
|
|
23
|
+
enableInputBox: boolean;
|
|
16
24
|
displayValues?: boolean;
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
hasNotApplicableOption: boolean;
|
|
26
|
+
sliderStyle?: 'standard' | 'graphics';
|
|
19
27
|
};
|
|
20
28
|
|
|
21
|
-
type
|
|
22
|
-
min: number;
|
|
29
|
+
type SliderMatrixStatementRow = StatementRow & {
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
step: number;
|
|
33
|
+
defaultValue?: number;
|
|
23
34
|
};
|
|
24
|
-
|
|
25
|
-
type TickValue = { value: number; label: string; color: string; };
|
|
26
35
|
```
|
|
27
36
|
|
|
28
37
|
## Answer Shape: `MatrixRowAnswers`
|
|
38
|
+
|
|
29
39
|
```typescript
|
|
30
|
-
{ "
|
|
31
|
-
// number = slider
|
|
40
|
+
{ "486d2930-4b96-4ccd-abf7-19fe0fc63f57": 5, "4fba0abf-...": "N/A" }
|
|
41
|
+
// number = slider value; 'N/A' = not applicable (string, not null)
|
|
32
42
|
```
|
|
33
43
|
|
|
44
|
+
Skip logic field `SLIDER` uses values like `"rowId_tickValue"` (e.g. `"486d2930-..._5"`).
|
|
45
|
+
|
|
34
46
|
## Rendering Guidance
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- `
|
|
47
|
+
|
|
48
|
+
- Copy portable `SliderMatrixScale` — always uses `CustomSliderTrack` per row
|
|
49
|
+
- Shared header: colored badges from `ticks.marks` via `tickColorFromIndex`
|
|
50
|
+
- Optional `scaleAnchorLabels` above tick badges
|
|
51
|
+
- `sliderStyle === 'graphics'` → emoji thumb on `CustomSliderTrack`
|
|
52
|
+
- `enableInputBox` → numeric input + stepper buttons per row
|
|
53
|
+
- `hasNotApplicableOption` → checkbox; value `'N/A'` disables slider
|
|
41
54
|
|
|
42
55
|
## SDK Integration Checklist (agent)
|
|
43
56
|
|
|
57
|
+
- [ ] Copied templates before customizing
|
|
44
58
|
- [ ] Type literal: `QUESTION_TYPE.SLIDER_MATRIX`
|
|
45
|
-
- [ ] Answer stored as: `MatrixRowAnswers` —
|
|
46
|
-
- [ ] onAction CHANGE
|
|
47
|
-
- [ ] Visibility helper: `getVisibleStatementRowsForAnswers(
|
|
48
|
-
- [ ]
|
|
49
|
-
- [ ] Logic features: skip ✓ display ✓ answer ✓ (rows) variants ✓ numbering ✓
|
|
50
|
-
- [ ] Performance: debounce drag events if partial save enabled; one slider per visible row
|
|
59
|
+
- [ ] Answer stored as: `MatrixRowAnswers` — number or `'N/A'` per row
|
|
60
|
+
- [ ] onAction CHANGE: `{ questionId, answerValue: MatrixRowAnswers }`
|
|
61
|
+
- [ ] Visibility helper: `getVisibleStatementRowsForAnswers(..., customFieldValues?)`
|
|
62
|
+
- [ ] Statement text in 25% row column via `dangerouslySetInnerHTML`
|
|
51
63
|
- [ ] Matrix row: [`question-type-sdk-matrix.md#slider_matrix`](../../00-integration/question-type-sdk-matrix.md#slider_matrix)
|
|
@@ -2,44 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
> **Type**: `'FILE_UPLOAD'` | **API Source**: `FILE_UPLOAD`
|
|
4
4
|
> **Mapper**: `fileUploadMapper.ts` | **Component**: `FileUploadScale`
|
|
5
|
+
> **Upload flow**: [`00-integration/file-upload-aws.md`](../../00-integration/file-upload-aws.md)
|
|
5
6
|
|
|
6
7
|
## TypeScript Type
|
|
7
8
|
|
|
8
9
|
```typescript
|
|
9
10
|
type FileUploadQuestion = QuestionBase & {
|
|
10
11
|
type: 'FILE_UPLOAD';
|
|
11
|
-
uploadMessage?: string;
|
|
12
|
-
supportedFileFormats?: string[];
|
|
13
|
-
maxFileCount?: number;
|
|
14
|
-
fileSizeLimit?: number;
|
|
12
|
+
uploadMessage?: string;
|
|
13
|
+
supportedFileFormats?: string[];
|
|
14
|
+
maxFileCount?: number;
|
|
15
|
+
fileSizeLimit?: number;
|
|
15
16
|
fileSizeLimitType?: 'PER_FILE' | 'IN_TOTAL';
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** Stored in state.answers — required for submit */
|
|
20
|
+
type UploadedFileAnswer = {
|
|
21
|
+
id: string;
|
|
22
|
+
mediaUrl: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
size?: number;
|
|
21
26
|
};
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
## Answer Shape: `
|
|
25
|
-
|
|
29
|
+
## Answer Shape: `UploadedFileAnswer[]`
|
|
30
|
+
|
|
31
|
+
Submitted via `quesIdVsAttachmentDetails` (NOT `questionToAnswers`). SDK submit formatter only accepts objects with `id` and `mediaUrl` — **raw browser `File[]` is dropped at submit**.
|
|
32
|
+
|
|
33
|
+
## Two-phase client flow
|
|
34
|
+
|
|
35
|
+
1. **Pick + validate locally** — extension, count, size (`PER_FILE` / `IN_TOTAL`)
|
|
36
|
+
2. **Upload each file** — `POST /get-upload-url` → presigned PUT → collect `{ id, mediaUrl, name, mimeType, size }`
|
|
37
|
+
3. **`onAction(CHANGE)`** — pass `UploadedFileAnswer[]` to SDK
|
|
38
|
+
|
|
39
|
+
See [`file-upload-aws.md`](../../00-integration/file-upload-aws.md) for API contract.
|
|
26
40
|
|
|
27
41
|
## Rendering Guidance
|
|
28
|
-
|
|
42
|
+
|
|
43
|
+
- Drag-and-drop zone + click-to-upload
|
|
29
44
|
- Show accepted formats from `supportedFileFormats`
|
|
30
|
-
- Set `<input type="file" accept=".pdf,.png
|
|
31
|
-
- Enforce `maxFileCount`
|
|
32
|
-
- Show file list with name, size,
|
|
33
|
-
-
|
|
34
|
-
|
|
45
|
+
- Set `<input type="file" accept=".pdf,.png,...">` from formats
|
|
46
|
+
- Enforce `maxFileCount` and `fileSizeLimit` before upload
|
|
47
|
+
- Show file list with name, size, remove button, uploading spinner per file
|
|
48
|
+
- `onSelect(undefined)` when all files removed
|
|
49
|
+
|
|
50
|
+
## Skip logic
|
|
51
|
+
|
|
52
|
+
Presence-only (`EXISTS` / `MISSING`) — evaluated on page entry; no discrete value-change events required.
|
|
35
53
|
|
|
36
54
|
## SDK Integration Checklist (agent)
|
|
37
55
|
|
|
38
56
|
- [ ] Type literal: `QUESTION_TYPE.FILE_UPLOAD`
|
|
39
|
-
- [ ] Answer stored as: `
|
|
40
|
-
- [ ] onAction CHANGE
|
|
41
|
-
- [ ]
|
|
42
|
-
- [ ] Custom fields: skip/display rules only — no visibility helper
|
|
43
|
-
- [ ] Logic features: skip ✓ display ✓ answer ✗ variants ✓ numbering ✓
|
|
44
|
-
- [ ] Performance: upload to presigned URL before CHANGE; show progress UI
|
|
57
|
+
- [ ] Answer stored as: `UploadedFileAnswer[]` after upload completes
|
|
58
|
+
- [ ] onAction CHANGE: `{ questionId, answerValue: UploadedFileAnswer[] }`
|
|
59
|
+
- [ ] Do **not** store raw `File[]` as final answer
|
|
45
60
|
- [ ] Matrix row: [`question-type-sdk-matrix.md#file_upload`](../../00-integration/question-type-sdk-matrix.md#file_upload)
|
|
@@ -49,10 +49,20 @@ Each type is **standalone** — open its file in exported SDK types for the comp
|
|
|
49
49
|
- **Answer:** `string` or `string[]` (`option.id` values)
|
|
50
50
|
- **Options:** `McqOption[]` with `id`, `optionLabel`
|
|
51
51
|
|
|
52
|
+
| Field | Type | API source |
|
|
53
|
+
|-------|------|------------|
|
|
54
|
+
| `selectionMode` | `'single' \| 'multiple'` | multi-select config |
|
|
55
|
+
| `defaultOptionIds?` | `string[]` | `DEFAULT_ANSWER` when enabled |
|
|
56
|
+
| `minSelections?` | `number` | `ANSWER_CONDITION` / `MIN_ANSWER` |
|
|
57
|
+
| `maxSelections?` | `number` | `ANSWER_CONDITION` / `MAX_ANSWER` |
|
|
58
|
+
| `answerConditionErrorMessage?` | `string` | `ANSWER_CONDITION` error message |
|
|
59
|
+
|
|
60
|
+
**Default answers:** On survey load, SDK `buildInitialAnswers` / `applyMcqDefaults` pre-populates `state.answers[questionId]` from `defaultOptionIds` (single: first id; multi: full array). Client binds UI checked state to `state.answers` — do not maintain separate default state. SDK re-applies defaults on validation/submit if answer is still empty.
|
|
61
|
+
|
|
52
62
|
### SDK Integration Checklist (agent)
|
|
53
63
|
|
|
54
64
|
- [ ] Type literal: `QUESTION_TYPE.MCQ`
|
|
55
|
-
- [ ] Answer stored as: `option.id` (string/number) or array when
|
|
65
|
+
- [ ] Answer stored as: `option.id` (string/number) or array when `selectionMode === 'multiple'`
|
|
56
66
|
- [ ] onAction CHANGE payload: `{ questionId, answerValue: optionId | optionId[] }`
|
|
57
67
|
- [ ] Visibility helper: `getVisibleMcqOptionsForAnswers(question, answers, allQuestions, customFieldValues?)`
|
|
58
68
|
- [ ] Custom fields: 4th arg when logic references CRM fields
|
|
@@ -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
|
-
|
|
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.
|
|
@@ -17,8 +17,16 @@
|
|
|
17
17
|
| `midLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
18
18
|
| `maxLabel` | ✅ | Absolute positioned in Question.tsx |
|
|
19
19
|
| `midLabelIndex` | ✅ | Mid label `left` percentage |
|
|
20
|
-
| `reverseScaleOrder` |
|
|
21
|
-
| `buttonVariant` | ❌ | Client
|
|
20
|
+
| `reverseScaleOrder` | ✅ | Reverse option index before selection/color lookup |
|
|
21
|
+
| `buttonVariant` | ❌ | Client uses dual-layer radio layout |
|
|
22
|
+
|
|
23
|
+
## Answer logic on scale points
|
|
24
|
+
|
|
25
|
+
When scale points have answer logic, filter with `getVisibleScaleColumnsForAnswers(question, answers, allQuestions, customFieldValues?)` before render.
|
|
26
|
+
|
|
27
|
+
## Implementation Recipe — reverseScaleOrder
|
|
28
|
+
|
|
29
|
+
When `reverseScaleOrder === true`, map visual index `i` to stored index `options.length - 1 - i` before deriving submit value and badge color.
|
|
22
30
|
|
|
23
31
|
## Answer Mutation
|
|
24
32
|
|
|
@@ -36,3 +44,21 @@ See [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
|
|
|
36
44
|
- [ ] Two-row grid with `gridTemplateColumns: repeat(options.length, 1fr)`
|
|
37
45
|
- [ ] Only track row is clickable
|
|
38
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
|
|
@@ -2,44 +2,114 @@
|
|
|
2
2
|
|
|
3
3
|
> **SDK type:** `McqQuestion` — `fetchSurvey/types/mcq.ts`
|
|
4
4
|
> **Component blueprint:** `01-components/02-question.md`
|
|
5
|
+
> **Dispatcher:** inline in `Question.tsx` (see [`templates/Question.tsx`](../templates/Question.tsx))
|
|
5
6
|
|
|
6
7
|
## Config Inventory
|
|
7
8
|
|
|
8
|
-
| Field |
|
|
9
|
-
|
|
10
|
-
| `options[]` |
|
|
11
|
-
| `options[].optionLabel` |
|
|
12
|
-
| `options[].id` |
|
|
13
|
-
| `
|
|
9
|
+
| Field | UI impact |
|
|
10
|
+
|-------|-----------|
|
|
11
|
+
| `options[]` | One card per visible option |
|
|
12
|
+
| `options[].optionLabel` | HTML via `dangerouslySetInnerHTML` |
|
|
13
|
+
| `options[].id` | **Submit value** — answer is `option.id` |
|
|
14
|
+
| `selectionMode` | `'single'` → radio cards; `'multiple'` → checkbox cards + `string[]` answer |
|
|
15
|
+
| `defaultOptionIds?` | Pre-selected on load (SDK seeds `state.answers`) |
|
|
16
|
+
| `minSelections?` / `maxSelections?` | Answer-condition count (SDK validates on NEXT) |
|
|
17
|
+
| `answerConditionErrorMessage?` | Custom validation banner text |
|
|
18
|
+
| `requiredErrorMessage?` | Required-field error text |
|
|
14
19
|
|
|
15
|
-
##
|
|
20
|
+
## Selection mode (not `isMultiSelect`)
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Use `question.selectionMode` from the SDK — **never** `isMultiSelect`.
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
| `selectionMode` | UI | Answer shape |
|
|
25
|
+
|-----------------|-----|--------------|
|
|
26
|
+
| `'single'` (default) | Radio cards; one selected | `string` (option `id`) |
|
|
27
|
+
| `'multiple'` | Checkbox cards; toggle on click | `string[]` of option ids |
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
Multi-select toggle pattern:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const handleToggle = (optionId: string) => {
|
|
33
|
+
const next = selectedIds.includes(optionId)
|
|
34
|
+
? selectedIds.filter(id => id !== optionId)
|
|
35
|
+
: question.maxSelections && selectedIds.length >= question.maxSelections
|
|
36
|
+
? selectedIds // block add at max
|
|
37
|
+
: [...selectedIds, optionId];
|
|
38
|
+
onSelect(next);
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Filter options before render: `getVisibleMcqOptionsForAnswers(question, allAnswers, allQuestions, customFieldValues?)`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Implementation Recipe — Default answers
|
|
47
|
+
|
|
48
|
+
| Concern | SDK behavior | Client UI contract |
|
|
49
|
+
|---------|--------------|-------------------|
|
|
50
|
+
| **Config** | API `DEFAULT_ANSWER` → `question.defaultOptionIds?: string[]` | Inventory in implementation plan §4b |
|
|
51
|
+
| **Initial load** | `buildInitialAnswers` / `applyMcqDefaults` writes into `state.answers` | Bind checked state to `selectedValue` (`state.answers[question.id]`) |
|
|
52
|
+
| **Single-select** | Default = `defaultOptionIds[0]` | Pre-selected radio when present in `state.answers` |
|
|
53
|
+
| **Multi-select** | Default = full `defaultOptionIds` array | Pre-checked boxes for each id |
|
|
54
|
+
| **User override** | `applyMcqDefaults` never overwrites user selection | Toggle via `onAction(CHANGE)` |
|
|
55
|
+
| **Validation / submit** | SDK re-applies defaults in `validateCurrentPage` and submit if empty | Client does **not** re-apply defaults at NEXT/SUBMIT |
|
|
56
|
+
| **Answer logic** | Hidden defaults may exist in `state.answers` until SDK sanitizes | Show checked state only for **visible** options |
|
|
57
|
+
| **Template fallback** | `Question.tsx` uses `selectedValue ?? defaultOptionIds` defensively | Prefer `state.answers`; fallback is display-only |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Implementation Recipe — Answer conditions
|
|
62
|
+
|
|
63
|
+
When API `ANSWER_CONDITION` is enabled, SDK maps:
|
|
64
|
+
|
|
65
|
+
- `minSelections` — minimum selected count (multi-select)
|
|
66
|
+
- `maxSelections` — maximum selected count (multi-select)
|
|
67
|
+
- `answerConditionErrorMessage` — shown in `state.validationErrors[questionId]`
|
|
68
|
+
|
|
69
|
+
**Client must:**
|
|
70
|
+
|
|
71
|
+
1. Render `validationError` banner when SDK sets an error (do not duplicate count validation).
|
|
72
|
+
2. Block adding options when `selectedIds.length >= maxSelections` in multi-select UI.
|
|
73
|
+
3. Pass through `onAction(CHANGE)` with updated array — SDK validates on NEXT.
|
|
74
|
+
|
|
75
|
+
Skip logic may use `NUM_OF_SELECTED_OPTIONS` — see [`logic-fields-catalog.md`](../00-integration/logic-fields-catalog.md).
|
|
76
|
+
|
|
77
|
+
---
|
|
25
78
|
|
|
26
79
|
## Answer Mutation
|
|
27
80
|
|
|
28
81
|
```typescript
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
// Single-select
|
|
83
|
+
onSelect(option.id)
|
|
84
|
+
onAction({ type: 'CHANGE', payload: { questionId, answerValue: string } })
|
|
85
|
+
|
|
86
|
+
// Multi-select
|
|
87
|
+
onSelect(selectedIds) // string[]
|
|
88
|
+
onAction({ type: 'CHANGE', payload: { questionId, answerValue: string[] } })
|
|
31
89
|
```
|
|
32
90
|
|
|
33
91
|
## Agent Checklist
|
|
34
92
|
|
|
35
|
-
- [ ]
|
|
36
|
-
- [ ]
|
|
37
|
-
- [ ] Selected:
|
|
38
|
-
- [ ]
|
|
39
|
-
- [ ]
|
|
93
|
+
- [ ] Use `selectionMode`, not `isMultiSelect`
|
|
94
|
+
- [ ] Full-width card options; custom circular radio / checkbox indicators
|
|
95
|
+
- [ ] Selected: use `var(--cfm-input-focus-ring)` border + `var(--cfm-mcq-selected-bg)` / `var(--cfm-matrix-selected)` — not hardcoded `#e20074` / `#fdf2f8`
|
|
96
|
+
- [ ] Multi-select: `string[]` answer; block at `maxSelections`
|
|
97
|
+
- [ ] Defaults: bind to `state.answers`; pre-selected visual on load when `defaultOptionIds` set
|
|
98
|
+
- [ ] `getVisibleMcqOptionsForAnswers` with optional `customFieldValues` 4th arg
|
|
99
|
+
- [ ] Store `option.id` — never label text
|
|
100
|
+
- [ ] Matrix row: [`question-type-sdk-matrix.md#mcq`](../00-integration/question-type-sdk-matrix.md#mcq)
|
|
40
101
|
|
|
41
|
-
##
|
|
102
|
+
## Wizard customization (Phase 5 — required)
|
|
42
103
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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:** `
|
|
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)`
|