@explorer02/cfm-survey-sdk 0.2.1 → 0.2.3
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 +18 -18
- package/dist/cli/index.mjs +14 -14
- package/dist/index.d.mts +465 -580
- package/dist/index.d.ts +465 -580
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -4
- package/postinstall.js +96 -69
- package/templates/AGENT.md +18 -8
- package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
- package/templates/docs/00-integration/answer-logic-and-navigation.md +50 -0
- package/templates/docs/00-integration/aws-deploy.md +281 -0
- package/templates/docs/00-integration/client-integration-guide.md +351 -0
- package/templates/docs/00-integration/component-checklist.md +55 -22
- package/templates/docs/00-integration/constraints.md +49 -4
- package/templates/docs/00-integration/custom-field-logic-and-navigation.md +174 -0
- package/templates/docs/00-integration/display-logic-and-navigation.md +64 -0
- package/templates/docs/00-integration/end-page-logic.md +64 -0
- 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 +98 -0
- package/templates/docs/00-integration/placeholders-and-tokens.md +116 -0
- package/templates/docs/00-integration/progress.md +61 -0
- package/templates/docs/00-integration/question-display-variants.md +65 -0
- package/templates/docs/00-integration/question-numbering.md +51 -0
- package/templates/docs/00-integration/question-type-sdk-matrix.md +244 -0
- package/templates/docs/00-integration/setup.md +84 -0
- package/templates/docs/00-integration/skip-logic-and-navigation.md +80 -0
- package/templates/docs/00-integration/survey-lifecycle-analytics.md +94 -0
- package/templates/docs/00-integration/useSurveySDK.md +151 -23
- package/templates/docs/01-components/01-survey-page.md +76 -54
- package/templates/docs/01-components/02-question.md +61 -58
- package/templates/docs/01-components/03-rating-scale.md +11 -1
- package/templates/docs/01-components/04-star-rating-scale.md +45 -0
- package/templates/docs/01-components/05-csat-matrix-scale.md +20 -11
- package/templates/docs/01-components/06-likert-matrix-scale.md +14 -4
- package/templates/docs/01-components/07-slider-matrix-scale.md +11 -2
- package/templates/docs/01-components/08-file-upload-scale.md +23 -51
- package/templates/docs/01-components/09-custom-slider-track.md +2 -2
- package/templates/docs/01-components/11-progress-bar.md +10 -4
- package/templates/docs/01-components/13-matrix-dropdown.md +29 -32
- package/templates/docs/01-components/14-intro-page.md +34 -0
- package/templates/docs/01-components/15-end-page.md +31 -0
- package/templates/docs/01-components/16-paused-page.md +27 -0
- package/templates/docs/01-components/17-heatmap-scale.md +75 -0
- package/templates/docs/01-components/18-rank-order-scale.md +93 -0
- package/templates/docs/01-components/README.md +42 -36
- package/templates/docs/02-reference/config-field-index.md +141 -139
- package/templates/docs/02-reference/exports.md +149 -0
- package/templates/docs/02-reference/question-types/01-rating.md +31 -32
- package/templates/docs/02-reference/question-types/04-csat.md +51 -42
- package/templates/docs/02-reference/question-types/05-rating-scale.md +16 -11
- package/templates/docs/02-reference/question-types/06-slider.md +2 -2
- package/templates/docs/02-reference/question-types/07-matrix-cfm.md +44 -30
- package/templates/docs/02-reference/question-types/08-matrix-csat.md +4 -26
- package/templates/docs/02-reference/question-types/09-matrix-rating.md +29 -13
- package/templates/docs/02-reference/question-types/10-slider-matrix.md +15 -4
- package/templates/docs/02-reference/question-types/11-file-upload.md +43 -17
- package/templates/docs/02-reference/question-types/12-heatmap.md +59 -0
- package/templates/docs/02-reference/question-types/13-rank-order.md +101 -0
- package/templates/docs/02-reference/question-types/README.md +96 -47
- package/templates/docs/02-reference/routing-table.md +33 -18
- package/templates/docs/02-reference/value-derivation.md +63 -0
- package/templates/docs/03-ui-specs/00-question-shell.md +19 -88
- package/templates/docs/03-ui-specs/01-rating.md +18 -52
- package/templates/docs/03-ui-specs/02-radio.md +80 -37
- package/templates/docs/03-ui-specs/03-text.md +2 -2
- package/templates/docs/03-ui-specs/04-csat.md +68 -33
- package/templates/docs/03-ui-specs/05-rating-scale.md +21 -18
- package/templates/docs/03-ui-specs/06-slider.md +3 -1
- package/templates/docs/03-ui-specs/07-matrix-cfm.md +44 -25
- package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +19 -43
- package/templates/docs/03-ui-specs/09-slider-matrix.md +18 -1
- package/templates/docs/03-ui-specs/10-file-upload.md +42 -21
- package/templates/docs/03-ui-specs/11-text-and-media.md +29 -29
- package/templates/docs/03-ui-specs/12-survey-chrome.md +58 -14
- package/templates/docs/03-ui-specs/13-heatmap.md +80 -0
- package/templates/docs/03-ui-specs/14-rank-order.md +128 -0
- package/templates/docs/03-ui-specs/README.md +16 -14
- package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
- package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +5 -0
- package/templates/docs/MANIFEST.json +476 -69
- package/templates/docs/index.md +82 -30
- package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
- package/templates/docs/templates/MatrixDropdown.tsx +216 -0
- package/templates/docs/templates/Question.tsx +221 -154
- package/templates/docs/templates/RankOrderScale.tsx +353 -0
- package/templates/docs/templates/deploy-to-aws.sh +114 -0
- package/templates/docs/templates/heatmapCoords.ts +58 -0
- package/templates/docs/templates/implementation_plan.md +126 -67
- package/templates/docs/templates/survey-inventory.schema.json +77 -0
- package/templates/docs/templates/surveyUiIcons.tsx +52 -0
- package/templates/docs/templates/verify-agent-build.sh +85 -0
- package/templates/docs/01-components/04-csat-scale.md +0 -40
- package/templates/docs/02-question-types/01-rating.md +0 -44
package/templates/docs/index.md
CHANGED
|
@@ -4,23 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
Master guide for AI coding agents. Machine-readable index: `MANIFEST.json`.
|
|
6
6
|
|
|
7
|
+
**Installed package path:** `node_modules/@explorer02/cfm-survey-sdk/templates/docs/` (docs ship inside `templates/` — no postinstall copy needed).
|
|
8
|
+
|
|
7
9
|
## Documentation Layout
|
|
8
10
|
|
|
9
11
|
```
|
|
10
|
-
docs/
|
|
12
|
+
templates/docs/ ← installed npm package layout
|
|
13
|
+
packages/sdk/templates/docs/ ← monorepo layout
|
|
11
14
|
├── index.md ← YOU ARE HERE
|
|
12
15
|
├── MANIFEST.json ← Phase + question-type routing (for tooling)
|
|
13
16
|
├── 00-integration/ ← Setup, hook contract, constraints
|
|
14
|
-
├── 01-components/ ← Wiring blueprints (read
|
|
15
|
-
├── 02-reference/ ← Routing table
|
|
16
|
-
├── 03-ui-specs/ ← Authoritative UI specs
|
|
17
|
+
├── 01-components/ ← Wiring blueprints (read per type from MANIFEST)
|
|
18
|
+
├── 02-reference/ ← Routing table, exports, config-field-index
|
|
19
|
+
├── 03-ui-specs/ ← Authoritative UI specs
|
|
17
20
|
└── templates/
|
|
18
21
|
├── implementation_plan.md
|
|
22
|
+
├── survey-inventory.schema.json
|
|
23
|
+
├── verify-agent-build.sh
|
|
24
|
+
├── deploy-to-aws.sh
|
|
25
|
+
├── heatmapCoords.ts
|
|
19
26
|
└── Question.tsx ← Canonical dispatcher (copy before building)
|
|
20
27
|
```
|
|
21
28
|
|
|
22
29
|
**Import rule:** Use whatever package name appears in the project's `package.json` (`@explorer02/cfm-survey-sdk` or `@repo/sdk`).
|
|
23
30
|
|
|
31
|
+
## Do Not Read
|
|
32
|
+
|
|
33
|
+
- Internal SDK mapper/source paths (`packages/sdk/src/**` or `node_modules/.../src/**`)
|
|
34
|
+
- `cfm-sdk init`, `survey-config.json`, or any setup wizard
|
|
35
|
+
- Stale `apps/client` code without verifying type literals against `MANIFEST.json` `typeLiterals`
|
|
36
|
+
|
|
24
37
|
---
|
|
25
38
|
|
|
26
39
|
## Client Inputs (3 only)
|
|
@@ -37,7 +50,9 @@ docs/
|
|
|
37
50
|
|
|
38
51
|
### PHASE 0: ORIENT
|
|
39
52
|
|
|
40
|
-
- Read this file and `AGENT.md
|
|
53
|
+
- Read this file and `AGENT.md`:
|
|
54
|
+
- **Monorepo:** `packages/sdk/templates/AGENT.md`
|
|
55
|
+
- **Installed npm:** `node_modules/@explorer02/cfm-survey-sdk/templates/AGENT.md`
|
|
41
56
|
- Detect framework: read `package.json` — Next.js / Vite / CRA (see `00-integration/setup.md`)
|
|
42
57
|
- Extract branding and layout from client prompt
|
|
43
58
|
- For existing apps: read project structure; do not blindly scaffold
|
|
@@ -56,60 +71,93 @@ Ask: *"Please provide your survey instance ID (JWT token)."*
|
|
|
56
71
|
|
|
57
72
|
### PHASE 3: SDK CONTRACT
|
|
58
73
|
|
|
59
|
-
Read: `00-integration/useSurveySDK.md`
|
|
74
|
+
Read: `00-integration/client-integration-guide.md` (start here), `00-integration/question-type-sdk-matrix.md`, `00-integration/useSurveySDK.md`, `02-reference/exports.md`, `00-integration/skip-logic-and-navigation.md`, `00-integration/display-logic-and-navigation.md`, `00-integration/question-display-variants.md`, `00-integration/question-numbering.md`, `00-integration/placeholders-and-tokens.md`, `00-integration/partial-save-and-recovery.md`, `00-integration/survey-lifecycle-analytics.md`, `00-integration/end-page-logic.md`, `00-integration/answer-logic-and-navigation.md`, `00-integration/custom-field-logic-and-navigation.md`, `00-integration/progress.md`, `02-reference/value-derivation.md`, `00-integration/logic-fields-catalog.md`, `00-integration/analytics-events-catalog.md`
|
|
60
75
|
|
|
61
|
-
Understand `useSurveySDK`, `onAction`,
|
|
76
|
+
Understand `useSurveySDK`, `onAction`, `state.answers`. Do **not** read internal mapper/source paths.
|
|
62
77
|
|
|
63
78
|
Optional: read `02-reference/routing-table.md` for type overview.
|
|
64
79
|
|
|
65
80
|
### PHASE 4: PLAN + INGEST (CRITICAL)
|
|
66
81
|
|
|
67
|
-
1.
|
|
68
|
-
2.
|
|
69
|
-
3. **
|
|
70
|
-
4. For **each type in the survey**, read
|
|
71
|
-
5.
|
|
72
|
-
6. Copy `docs/templates/
|
|
73
|
-
7.
|
|
82
|
+
1. Fetch survey with `instanceId`; extract unique `question.type` values from `survey.pages`
|
|
83
|
+
2. Write inventory to `implementation_plan.md` §3b (validate against `templates/survey-inventory.schema.json`)
|
|
84
|
+
3. Read **always**: `component-checklist.md`, `00-integration/question-type-sdk-matrix.md`, `03-ui-specs/00-question-shell.md`, `03-ui-specs/12-survey-chrome.md`, `01-components/01-survey-page.md`, `01-components/02-question.md`, `01-components/14-intro-page.md`, `01-components/15-end-page.md`, `01-components/16-paused-page.md`
|
|
85
|
+
4. For **each type in the survey**, read MANIFEST `questionTypes.{type}` → blueprint + reference + uiSpec; cross-check the matching row in `question-type-sdk-matrix.md`
|
|
86
|
+
5. When config flags match MANIFEST `sharedUiSpecTriggers` (e.g. `gridLayout:dropdown`, `displayStyle:graphics`) → read matching `03-ui-specs/shared/` specs
|
|
87
|
+
6. Copy `docs/templates/implementation_plan.md` → `./implementation_plan.md` and fill every section
|
|
88
|
+
7. Copy `docs/templates/Question.tsx` as the dispatcher starting point
|
|
89
|
+
8. **Auto-proceed to Phase 5** — do not wait for approval
|
|
90
|
+
|
|
91
|
+
**Fallback:** If survey fetch fails, read all `01-components/*.md` blueprints before coding.
|
|
74
92
|
|
|
75
93
|
### PHASE 5: BUILD
|
|
76
94
|
|
|
77
|
-
Read: `00-integration/constraints.md`, `00-integration/component-checklist.md`
|
|
95
|
+
Read: `00-integration/constraints.md`, `00-integration/component-checklist.md`, `00-integration/question-type-sdk-matrix.md`
|
|
78
96
|
|
|
79
|
-
Create all required scale component files **before** wiring `Question.tsx`. For each question in the survey:
|
|
97
|
+
Create all required scale component files **before** wiring `Question.tsx`. **Copy portable templates** from `docs/templates/` when listed in `component-checklist.md` (`RankOrderScale.tsx`, `MatrixDropdown.tsx`, `CustomSliderTrack.tsx`, `surveyUiIcons.tsx`, `heatmapCoords.ts`). For each question in the survey:
|
|
80
98
|
|
|
81
|
-
1. Read `
|
|
82
|
-
2. Read `
|
|
83
|
-
|
|
99
|
+
1. Read the matching row in `question-type-sdk-matrix.md` (answer shape, visibility helper, performance)
|
|
100
|
+
2. Read `02-reference/question-types/` doc for data shape (MANIFEST `reference`)
|
|
101
|
+
2. Read matching `03-ui-specs/` (MANIFEST `uiSpec`)
|
|
102
|
+
3. If `gridLayout`, `displayStyle`, or `statementLayout` triggers shared specs → read `03-ui-specs/shared/`
|
|
84
103
|
4. Cross-check `02-reference/config-field-index.md` for every non-default field
|
|
85
|
-
5. Implement per ui-spec —
|
|
104
|
+
5. Implement per ui-spec — trust SDK Integration Checklist in blueprint/reference + `templates/Question.tsx` + exported SDK types + `02-reference/value-derivation.md`
|
|
86
105
|
|
|
87
106
|
Implement components per blueprints + UI specs. Use exported types:
|
|
88
107
|
|
|
89
108
|
```typescript
|
|
90
|
-
import { useSurveySDK, ACTIONS, type SurveyAction } from '@explorer02/cfm-survey-sdk';
|
|
109
|
+
import { useSurveySDK, ACTIONS, QUESTION_TYPE, type SurveyAction } from '@explorer02/cfm-survey-sdk';
|
|
91
110
|
```
|
|
92
111
|
|
|
112
|
+
**Dispatch on flat `question.type` only** — **11** `QUESTION_TYPE` literals, no `subType`. See `MANIFEST.json` `typeLiterals`.
|
|
113
|
+
|
|
114
|
+
**AWS deploy prep (when client prompt mentions AWS / CloudFront / S3 / EC2 / Sprinklr):** apply Next.js static export config **during this phase**, not at deploy time. Read `00-integration/setup.md#nextjs-aws-static-export` and `00-integration/aws-deploy.md` § Build Config. Fill `implementation_plan.md` §8.
|
|
115
|
+
|
|
93
116
|
### PHASE 6: VERIFY
|
|
94
117
|
|
|
95
118
|
Read: `00-integration/constraints.md` (troubleshooting)
|
|
96
119
|
|
|
120
|
+
Run: `bash templates/verify-agent-build.sh` (from project root, after copying script if needed)
|
|
121
|
+
|
|
97
122
|
Checklist:
|
|
98
123
|
|
|
99
|
-
- [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — covers all
|
|
100
|
-
- [ ]
|
|
124
|
+
- [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — covers all **11** `question.type` values
|
|
125
|
+
- [ ] Each matrix variant routes by its own type literal (`CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`) — **no `subType`**
|
|
101
126
|
- [ ] All scale components from `component-checklist.md` exist for types in survey
|
|
102
127
|
- [ ] **Zero stub text:** `grep -r "not yet implemented" src/ components/ app/` returns no matches
|
|
103
128
|
- [ ] Every question wrapper has `id={question.id}`
|
|
104
|
-
- [ ] No imports from `@explorer02/cfm-survey-sdk/src/...`
|
|
105
|
-
- [ ] `SurveyPage` handles loading / error / empty /
|
|
129
|
+
- [ ] No imports from `@explorer02/cfm-survey-sdk/src/...` or `@repo/sdk/src/...`
|
|
130
|
+
- [ ] `SurveyPage` handles loading / error / empty / all `state.phase` values / active content
|
|
131
|
+
- [ ] Hook uses `state.answers`, `state.canGoBack`, `state.primaryButtonType`, and `payload.answerValue`
|
|
132
|
+
- [ ] Stable `customFieldValues` ref when survey uses custom-field logic; passed to hook and `Question` visibility helpers
|
|
133
|
+
- [ ] `SURVEY_PLACEHOLDERS` stable ref; `surveyOptions` in `useMemo` (see `placeholders-and-tokens.md`)
|
|
134
|
+
- [ ] `SCROLL` + `RECORD_QUESTION_VIEW` lifecycle effects wired (see `survey-lifecycle-analytics.md`)
|
|
135
|
+
- [ ] Monorepo: `packages/sdk` built before client dev (`npm run build`)
|
|
106
136
|
- [ ] `npm run build` exits 0
|
|
137
|
+
- [ ] **AWS intent:** `out/index.html` exists; file count ≤ 500; Next.js static export configured (`output: export`, `assetPrefix: "./"`)
|
|
107
138
|
|
|
108
139
|
### PHASE 7: DEPLOY (INPUT 3)
|
|
109
140
|
|
|
110
141
|
Ask: *"Your survey is built and verified. Ready to deploy?"*
|
|
111
142
|
|
|
112
|
-
|
|
143
|
+
**Route by client prompt** (see MANIFEST `deployTargets` and `phases[7].selectTargetFromPrompt`):
|
|
144
|
+
|
|
145
|
+
| Prompt mentions | Target | Action |
|
|
146
|
+
|-----------------|--------|--------|
|
|
147
|
+
| AWS, CloudFront, S3, EC2, Sprinklr | **AWS** | Read `00-integration/aws-deploy.md`; set env vars; run 3-channel deploy or `bash templates/deploy-to-aws.sh` |
|
|
148
|
+
| Vercel or unspecified (default) | **Vercel** | `npx cfm-sdk deploy` (requires `VERCEL_TOKEN` or Vercel CLI) |
|
|
149
|
+
|
|
150
|
+
**AWS env vars (dev machine):**
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
export CFM_INSTANCE_ID="<same JWT as SurveyPage>"
|
|
154
|
+
export CFM_DEPLOY_API_BASE="http://43.204.26.213:3000"
|
|
155
|
+
export OUT_DIR="<absolute path to out/ or dist/>"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**AWS success:** print live CloudFront HTTPS URL from `/deploy/complete`; ask client to open in browser.
|
|
159
|
+
|
|
160
|
+
**Vercel success:** CLI prints production URL.
|
|
113
161
|
|
|
114
162
|
---
|
|
115
163
|
|
|
@@ -117,8 +165,12 @@ If yes: `npx cfm-sdk deploy` (preferred) or `npx vercel --prod --yes`
|
|
|
117
165
|
|
|
118
166
|
| When | Read |
|
|
119
167
|
|------|------|
|
|
120
|
-
| Always first | `index.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
|
|
121
|
-
|
|
|
122
|
-
|
|
|
168
|
+
| Always first | `index.md`, `00-integration/client-integration-guide.md`, `00-integration/question-type-sdk-matrix.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
|
|
169
|
+
| Lifecycle & placeholders | `survey-lifecycle-analytics.md`, `placeholders-and-tokens.md`, `partial-save-and-recovery.md`, `analytics-events-catalog.md` |
|
|
170
|
+
| Skip logic & progress | `skip-logic-and-navigation.md`, `display-logic-and-navigation.md`, `question-display-variants.md`, `question-numbering.md`, `end-page-logic.md`, `answer-logic-and-navigation.md`, `custom-field-logic-and-navigation.md`, `progress.md`, `logic-fields-catalog.md` |
|
|
171
|
+
| File upload submit | `file-upload-aws.md`, `03-ui-specs/10-file-upload.md` |
|
|
172
|
+
| Before any React code | `component-checklist.md`, `templates/Question.tsx`, MANIFEST `readAlways` docs |
|
|
173
|
+
| After instanceId fetch | Per-type docs from MANIFEST `questionTypes` (blocking: `CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`, `SLIDER_MATRIX`, `FILE_UPLOAD`) |
|
|
123
174
|
| Config audit | `02-reference/config-field-index.md` |
|
|
124
|
-
|
|
|
175
|
+
| AWS deploy | `00-integration/aws-deploy.md`, `setup.md#nextjs-aws-static-export`, `templates/deploy-to-aws.sh` |
|
|
176
|
+
| Tooling | `MANIFEST.json`, `templates/implementation_plan.md`, `templates/survey-inventory.schema.json` |
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portable CustomSliderTrack template — copy to src/components/CustomSliderTrack.tsx
|
|
5
|
+
* Also copy surveyUiIcons.tsx to the same folder (or src/lib/surveyUiIcons.tsx)
|
|
6
|
+
* Pass tickLabels from scaleColumns[].label for matrix scale label rendering (Day 19 fix)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { getEmojiForIndex } from './surveyUiIcons';
|
|
11
|
+
|
|
12
|
+
type CustomSliderTrackProps = {
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
htmlStep: number;
|
|
16
|
+
value: number;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
displayValues?: boolean;
|
|
19
|
+
hasSelectedValue: boolean;
|
|
20
|
+
sliderType?: 'graphics';
|
|
21
|
+
ticks?: number;
|
|
22
|
+
/** Pass scaleColumns.map(c => c.label) for visible column labels under slider */
|
|
23
|
+
tickLabels?: string[];
|
|
24
|
+
reverseScaleOrder?: boolean;
|
|
25
|
+
onChange: (v: number) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function CustomSliderTrack({
|
|
29
|
+
min,
|
|
30
|
+
max,
|
|
31
|
+
htmlStep,
|
|
32
|
+
value,
|
|
33
|
+
disabled,
|
|
34
|
+
sliderType,
|
|
35
|
+
ticks,
|
|
36
|
+
tickLabels,
|
|
37
|
+
reverseScaleOrder,
|
|
38
|
+
onChange,
|
|
39
|
+
}: CustomSliderTrackProps) {
|
|
40
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
41
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
42
|
+
|
|
43
|
+
const percentage = ((value - min) / (max - min)) * 100;
|
|
44
|
+
const tickIndex = Math.min(
|
|
45
|
+
Math.max(0, Math.round(value)),
|
|
46
|
+
tickLabels && tickLabels.length > 0 ? tickLabels.length - 1 : Math.round(value)
|
|
47
|
+
);
|
|
48
|
+
const tooltipLabel = tickLabels?.[tickIndex];
|
|
49
|
+
const tooltipText = tooltipLabel
|
|
50
|
+
? tooltipLabel.replace(/<[^>]*>/g, '')
|
|
51
|
+
: String(Math.round(value));
|
|
52
|
+
|
|
53
|
+
const themeColor = disabled ? '#9ca3af' : '#e20074';
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
style={{ position: 'relative', height: '32px', display: 'flex', alignItems: 'center', margin: '0 16px', opacity: disabled ? 0.5 : 1 }}
|
|
58
|
+
onMouseEnter={() => !disabled && setIsHovered(true)}
|
|
59
|
+
onMouseLeave={() => { setIsHovered(false); setIsDragging(false); }}
|
|
60
|
+
>
|
|
61
|
+
<div style={{
|
|
62
|
+
position: 'absolute', top: '50%', left: 0, right: 0, transform: 'translateY(-50%)',
|
|
63
|
+
height: '4px', borderRadius: '2px', backgroundColor: '#e5e7eb',
|
|
64
|
+
}} />
|
|
65
|
+
|
|
66
|
+
<div style={{
|
|
67
|
+
position: 'absolute', top: '50%', left: 0, width: `${percentage}%`, transform: 'translateY(-50%)',
|
|
68
|
+
height: '4px', borderRadius: '2px', backgroundColor: themeColor,
|
|
69
|
+
transition: isDragging ? 'none' : 'width 0.15s ease-in-out',
|
|
70
|
+
}} />
|
|
71
|
+
|
|
72
|
+
{ticks && ticks > 1 && Array.from({ length: ticks }).map((_, idx) => {
|
|
73
|
+
const tickPct = (idx / (ticks - 1)) * 100;
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
key={idx}
|
|
77
|
+
style={{
|
|
78
|
+
position: 'absolute', top: '50%', left: `${tickPct}%`, transform: 'translate(-50%, -50%)',
|
|
79
|
+
width: '1px', height: '8px', backgroundColor: '#d1d5db', pointerEvents: 'none',
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
|
|
85
|
+
<input
|
|
86
|
+
type="range"
|
|
87
|
+
min={min} max={max} step={htmlStep} value={value}
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
90
|
+
onMouseDown={() => !disabled && setIsDragging(true)}
|
|
91
|
+
onMouseUp={() => setIsDragging(false)}
|
|
92
|
+
onTouchStart={() => !disabled && setIsDragging(true)}
|
|
93
|
+
onTouchEnd={() => setIsDragging(false)}
|
|
94
|
+
style={{
|
|
95
|
+
position: 'absolute', top: '50%', left: '-16px', right: '-16px', width: 'calc(100% + 32px)', height: '100%',
|
|
96
|
+
transform: 'translateY(-50%)', opacity: 0, cursor: disabled ? 'not-allowed' : 'pointer', zIndex: 10, margin: 0,
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
{sliderType === 'graphics' ? (
|
|
101
|
+
<div style={{
|
|
102
|
+
position: 'absolute', top: '50%', left: `${percentage}%`, transform: 'translate(-50%, -50%)',
|
|
103
|
+
transition: isDragging ? 'none' : 'left 0.15s ease-in-out', pointerEvents: 'none', zIndex: 11,
|
|
104
|
+
opacity: disabled ? 0.5 : 1,
|
|
105
|
+
}}>
|
|
106
|
+
{getEmojiForIndex(
|
|
107
|
+
reverseScaleOrder
|
|
108
|
+
? (ticks || 10) - 1 - Math.min((ticks || 10) - 1, Math.max(0, Math.round(((value - min) / (max - min)) * ((ticks || 10) - 1))))
|
|
109
|
+
: Math.min((ticks || 10) - 1, Math.max(0, Math.round(((value - min) / (max - min)) * ((ticks || 10) - 1)))),
|
|
110
|
+
ticks || 10
|
|
111
|
+
) as React.ReactNode}
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<div style={{
|
|
115
|
+
position: 'absolute', top: '50%', left: `${percentage}%`, transform: 'translate(-50%, -50%)',
|
|
116
|
+
width: '18px', height: '18px', borderRadius: '50%', backgroundColor: '#ffffff',
|
|
117
|
+
border: `2.5px solid ${themeColor}`,
|
|
118
|
+
boxShadow: (isHovered || isDragging) && !disabled
|
|
119
|
+
? '0 0 0 6px rgba(226, 0, 116, 0.2), 0 1px 3px rgba(0,0,0,0.2)'
|
|
120
|
+
: '0 1px 3px rgba(0,0,0,0.2)',
|
|
121
|
+
pointerEvents: 'none',
|
|
122
|
+
transition: isDragging ? 'none' : 'left 0.15s ease-in-out, box-shadow 0.15s, border-color 0.2s',
|
|
123
|
+
boxSizing: 'border-box',
|
|
124
|
+
}} />
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{(isHovered || isDragging) && !disabled && (
|
|
128
|
+
<div style={{
|
|
129
|
+
position: 'absolute', bottom: '100%', left: `${percentage}%`, transform: 'translate(-50%, -10px)',
|
|
130
|
+
backgroundColor: '#1f2937', color: '#fff', fontSize: '13px', fontWeight: 600,
|
|
131
|
+
padding: '4px 10px', borderRadius: '6px', pointerEvents: 'none',
|
|
132
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', zIndex: 20,
|
|
133
|
+
}}>
|
|
134
|
+
{tooltipText}
|
|
135
|
+
<div style={{
|
|
136
|
+
position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
|
|
137
|
+
borderWidth: '5px', borderStyle: 'solid',
|
|
138
|
+
borderColor: '#1f2937 transparent transparent transparent',
|
|
139
|
+
}} />
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portable MatrixDropdown template — copy to src/components/MatrixDropdown.tsx
|
|
5
|
+
* Import SDK from your package.json: @explorer02/cfm-survey-sdk or @repo/sdk
|
|
6
|
+
* UI spec: docs/03-ui-specs/shared/matrix-dropdown.md
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
10
|
+
import type { ScaleColumn } from '@explorer02/cfm-survey-sdk';
|
|
11
|
+
import { matrixColumnStoredValue } from '@explorer02/cfm-survey-sdk';
|
|
12
|
+
|
|
13
|
+
type MatrixDropdownProps = {
|
|
14
|
+
options: ScaleColumn[];
|
|
15
|
+
value: string | number | null | Array<string | number | null> | undefined;
|
|
16
|
+
onChange: (val: string | number | null | Array<string | number | null>) => void;
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function MatrixDropdown({
|
|
22
|
+
options,
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
multiple = false,
|
|
26
|
+
placeholder = 'Select Answer',
|
|
27
|
+
}: MatrixDropdownProps) {
|
|
28
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
33
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
34
|
+
setIsOpen(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
38
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const multiValues: Array<string | number | null> = Array.isArray(value) ? value : [];
|
|
42
|
+
const selectedOptionSingle = !multiple
|
|
43
|
+
? options.find(o => matrixColumnStoredValue(o) === value)
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
const toggleMultiOption = (val: string | number | null) => {
|
|
47
|
+
if (val === null) {
|
|
48
|
+
if (multiValues.includes(null)) onChange([]);
|
|
49
|
+
else onChange([null]);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let nextVals = multiValues.filter(v => v !== null);
|
|
54
|
+
if (nextVals.includes(val)) {
|
|
55
|
+
nextVals = nextVals.filter(v => v !== val);
|
|
56
|
+
} else {
|
|
57
|
+
nextVals = [...nextVals, val];
|
|
58
|
+
}
|
|
59
|
+
onChange(nextVals);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const removeMultiOption = (e: React.MouseEvent, val: string | number | null) => {
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
onChange(multiValues.filter(v => v !== val));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const renderTrigger = () => {
|
|
68
|
+
if (multiple) {
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
72
|
+
style={{
|
|
73
|
+
display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px',
|
|
74
|
+
width: '100%', padding: '10px 14px', backgroundColor: '#fff',
|
|
75
|
+
border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
|
|
76
|
+
cursor: 'pointer', transition: 'all 0.2s', minHeight: '44px',
|
|
77
|
+
boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{multiValues.length === 0 ? (
|
|
81
|
+
<span style={{ fontSize: '15px', color: '#6b7280', flex: 1 }}>{placeholder}</span>
|
|
82
|
+
) : (
|
|
83
|
+
multiValues.map(v => {
|
|
84
|
+
const opt = options.find(o => matrixColumnStoredValue(o) === v);
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
key={String(v)}
|
|
88
|
+
style={{
|
|
89
|
+
display: 'flex', alignItems: 'center', gap: '6px',
|
|
90
|
+
backgroundColor: '#f3f4f6', padding: '4px 8px', borderRadius: '4px',
|
|
91
|
+
fontSize: '14px', color: '#374151', fontWeight: 500,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<span dangerouslySetInnerHTML={{ __html: opt?.label || String(v) }} />
|
|
95
|
+
<div
|
|
96
|
+
onClick={e => removeMultiOption(e, v)}
|
|
97
|
+
style={{
|
|
98
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
99
|
+
width: '16px', height: '16px', borderRadius: '50%', backgroundColor: '#e5e7eb',
|
|
100
|
+
color: '#6b7280', cursor: 'pointer', fontSize: '12px', fontWeight: 'bold',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
×
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})
|
|
108
|
+
)}
|
|
109
|
+
<div style={{ marginLeft: 'auto' }}>
|
|
110
|
+
<svg
|
|
111
|
+
style={{ width: '18px', height: '18px', color: '#6b7280', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
|
|
112
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
113
|
+
>
|
|
114
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
115
|
+
</svg>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
124
|
+
style={{
|
|
125
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
126
|
+
width: '100%', padding: '14px 16px', backgroundColor: '#fff',
|
|
127
|
+
border: `1px solid ${isOpen ? '#e20074' : '#d1d5db'}`, borderRadius: '8px',
|
|
128
|
+
cursor: 'pointer', transition: 'all 0.2s',
|
|
129
|
+
boxShadow: isOpen ? '0 0 0 3px rgba(226, 0, 116, 0.1)' : '0 1px 2px rgba(0,0,0,0.05)',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<span
|
|
133
|
+
style={{ fontSize: '15px', color: selectedOptionSingle ? '#111827' : '#6b7280', fontWeight: selectedOptionSingle ? 500 : 400 }}
|
|
134
|
+
dangerouslySetInnerHTML={{ __html: selectedOptionSingle ? selectedOptionSingle.label : placeholder }}
|
|
135
|
+
/>
|
|
136
|
+
<svg
|
|
137
|
+
style={{ width: '18px', height: '18px', color: '#6b7280', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
|
|
138
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
139
|
+
>
|
|
140
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
141
|
+
</svg>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div ref={containerRef} style={{ position: 'relative', width: '100%', userSelect: 'none' }}>
|
|
148
|
+
{renderTrigger()}
|
|
149
|
+
|
|
150
|
+
{isOpen && (
|
|
151
|
+
<div style={{
|
|
152
|
+
position: 'absolute', top: 'calc(100% + 8px)', left: 0, right: 0,
|
|
153
|
+
backgroundColor: '#fff', border: '1px solid #e5e5e5', borderRadius: '8px',
|
|
154
|
+
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
|
155
|
+
zIndex: 50, overflow: 'hidden', maxHeight: '280px', overflowY: 'auto',
|
|
156
|
+
}}>
|
|
157
|
+
{options.map(col => {
|
|
158
|
+
const submitValue = matrixColumnStoredValue(col);
|
|
159
|
+
const isSelected = multiple ? multiValues.includes(submitValue) : value === submitValue;
|
|
160
|
+
const isNaSelected = multiple && multiValues.includes(null);
|
|
161
|
+
const isDisabled = multiple && isNaSelected && submitValue !== null;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
key={col.id}
|
|
166
|
+
style={{
|
|
167
|
+
padding: '12px 16px', cursor: isDisabled ? 'not-allowed' : 'pointer', transition: 'background-color 0.15s',
|
|
168
|
+
backgroundColor: isSelected ? '#fdf2f8' : '#fff',
|
|
169
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
170
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
|
|
171
|
+
}}
|
|
172
|
+
onMouseEnter={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = '#fdf2f8'; }}
|
|
173
|
+
onMouseLeave={e => { if (!isDisabled) e.currentTarget.style.backgroundColor = isSelected ? '#fdf2f8' : '#fff'; }}
|
|
174
|
+
onClick={e => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
if (isDisabled) return;
|
|
177
|
+
if (multiple) {
|
|
178
|
+
toggleMultiOption(submitValue);
|
|
179
|
+
} else {
|
|
180
|
+
onChange(submitValue);
|
|
181
|
+
setIsOpen(false);
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1 }}>
|
|
186
|
+
{multiple && (
|
|
187
|
+
<div style={{
|
|
188
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
189
|
+
width: '18px', height: '18px', borderRadius: '4px',
|
|
190
|
+
border: isSelected ? '2px solid #e20074' : '2px solid #d1d5db',
|
|
191
|
+
backgroundColor: isSelected ? '#e20074' : '#fff',
|
|
192
|
+
flexShrink: 0, transition: 'all 0.15s',
|
|
193
|
+
}}>
|
|
194
|
+
{isSelected && (
|
|
195
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
196
|
+
<polyline points="20 6 9 17 4 12" />
|
|
197
|
+
</svg>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<span style={{ fontSize: '15px', fontWeight: isSelected ? 600 : 400, color: isSelected ? '#e20074' : '#374151', userSelect: 'none' }}
|
|
202
|
+
dangerouslySetInnerHTML={{ __html: col.label }} />
|
|
203
|
+
</div>
|
|
204
|
+
{!multiple && isSelected && (
|
|
205
|
+
<svg style={{ width: '18px', height: '18px', color: '#e20074', flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
206
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
207
|
+
</svg>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|