@explorer02/cfm-survey-sdk 0.2.2 → 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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/templates/AGENT.md +8 -4
  3. package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
  4. package/templates/docs/00-integration/client-integration-guide.md +1 -1
  5. package/templates/docs/00-integration/component-checklist.md +10 -0
  6. package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
  7. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  8. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  9. package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
  10. package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
  11. package/templates/docs/00-integration/setup.md +3 -3
  12. package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
  13. package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
  14. package/templates/docs/01-components/06-likert-matrix-scale.md +2 -1
  15. package/templates/docs/01-components/08-file-upload-scale.md +15 -52
  16. package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
  17. package/templates/docs/01-components/17-heatmap-scale.md +4 -4
  18. package/templates/docs/01-components/18-rank-order-scale.md +24 -31
  19. package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
  20. package/templates/docs/02-reference/question-types/README.md +11 -1
  21. package/templates/docs/03-ui-specs/01-rating.md +10 -2
  22. package/templates/docs/03-ui-specs/02-radio.md +80 -25
  23. package/templates/docs/03-ui-specs/04-csat.md +35 -5
  24. package/templates/docs/03-ui-specs/07-matrix-cfm.md +20 -4
  25. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +5 -1
  26. package/templates/docs/03-ui-specs/09-slider-matrix.md +17 -0
  27. package/templates/docs/03-ui-specs/10-file-upload.md +41 -20
  28. package/templates/docs/03-ui-specs/12-survey-chrome.md +38 -0
  29. package/templates/docs/03-ui-specs/13-heatmap.md +9 -2
  30. package/templates/docs/03-ui-specs/14-rank-order.md +98 -29
  31. package/templates/docs/03-ui-specs/README.md +7 -5
  32. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  33. package/templates/docs/MANIFEST.json +28 -3
  34. package/templates/docs/index.md +5 -4
  35. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  36. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  37. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  38. package/templates/docs/templates/implementation_plan.md +20 -0
  39. package/templates/docs/templates/survey-inventory.schema.json +28 -23
  40. package/templates/docs/templates/surveyUiIcons.tsx +52 -0
  41. package/templates/docs/templates/verify-agent-build.sh +32 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -20,20 +20,24 @@ Import from whatever appears in `package.json`:
20
20
  | 0 | Detect framework; extract branding from client prompt |
21
21
  | 1 | Scaffold React app — `00-integration/setup.md` |
22
22
  | 2 | Ask client for instanceId JWT |
23
- | 3 | Read `client-integration-guide.md` + `question-type-sdk-matrix.md` + hook/logic/lifecycle docs — full SDK contract |
24
- | 4 | Fetch survey; inventory types; read MANIFEST-driven docs per type + matrix row; copy `templates/Question.tsx` + `implementation_plan.md` |
25
- | 5 | Build UI — read matrix before any scale component; create files from `component-checklist.md`; dispatch on **11** flat `QUESTION_TYPE` literals; apply AWS static export if prompt requires |
23
+ | 3 | Read `client-integration-guide.md` + `question-type-sdk-matrix.md` + hook/logic/lifecycle/analytics docs — full SDK contract |
24
+ | 4 | Fetch survey; inventory types; read MANIFEST-driven docs per type; copy `templates/Question.tsx` + scale templates + `implementation_plan.md` |
25
+ | 5 | Build UI — copy templates from `docs/templates/`; read ui-spec Implementation Recipes; dispatch on **11** flat `QUESTION_TYPE` literals |
26
26
  | 6 | Run `templates/verify-agent-build.sh` |
27
- | 7 | On confirmation: **AWS prompt** → `00-integration/aws-deploy.md` + EC2 3-channel deploy; **Vercel prompt** → `npx cfm-sdk deploy` |
27
+ | 7 | On confirmation: **AWS prompt** → `aws-deploy.md`; **Vercel prompt** → `npx cfm-sdk deploy` |
28
28
 
29
29
  **Do NOT** run `cfm-sdk init`, read `survey-config.json`, or use any setup wizard.
30
30
 
31
31
  **Do NOT** render `"not yet implemented"` or default fallback stubs in `Question.tsx`. Copy `templates/docs/templates/Question.tsx` and create all required scale components.
32
32
 
33
+ **Copy scale templates from `docs/templates/`** before implementing from ui-spec recipes: `RankOrderScale.tsx`, `MatrixDropdown.tsx`, `CustomSliderTrack.tsx`, `surveyUiIcons.tsx`, `heatmapCoords.ts`.
34
+
33
35
  **Do NOT** route matrix questions by `subType` — use flat types: `CFM_MATRIX`, `CSAT_MATRIX`, `RATING_MATRIX`, `SLIDER_MATRIX`.
34
36
 
35
37
  **Read `question-type-sdk-matrix.md` before building any scale component** — it defines answer shapes, visibility helpers, and performance expectations for all 11 types.
36
38
 
39
+ **FILE_UPLOAD:** wire presigned upload per `00-integration/file-upload-aws.md` — raw `File[]` does not submit.
40
+
37
41
  **Monorepo:** run `npm run build` in `packages/sdk` before starting the client dev server.
38
42
 
39
43
  Follow the master index strictly. Do not guess SDK internals. Do not import from `/src/` paths.
@@ -0,0 +1,54 @@
1
+ # Analytics Events Catalog
2
+
3
+ > Consolidated SDK telemetry and client-wired lifecycle events. Most events log to console as `[SDK:ANALYTICS]` — **no client code required** unless noted.
4
+
5
+ See also: [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md) · [`client-integration-guide.md`](client-integration-guide.md) § Debug logging
6
+
7
+ ---
8
+
9
+ ## Client must wire
10
+
11
+ | Event / action | How | When |
12
+ |----------------|-----|------|
13
+ | Mark as started | `onAction({ type: 'SCROLL' })` once on first window scroll | First content engagement |
14
+ | Question viewed | `onAction({ type: 'RECORD_QUESTION_VIEW', payload: { questionId } })` via IntersectionObserver on `#question.id` | Question 50% visible |
15
+ | Partial save feedback | Monitor `partialSaveResults.isLoading` / `.error` | When `savePartialResponse: true` |
16
+
17
+ First `CHANGE` also triggers mark-as-started (SDK) — no extra client call.
18
+
19
+ ---
20
+
21
+ ## SDK-emitted (always — no `debug` flag required)
22
+
23
+ | Event | When | Metadata highlights |
24
+ |-------|------|---------------------|
25
+ | `fetch_survey_latency` | Survey GET completes | durationMs, status, payloadSize (UTF-8 KB) |
26
+ | `fetch_survey_error` | Survey GET fails | status, url, message |
27
+ | `submit_survey_latency` | Submit POST completes | durationMs, status |
28
+ | `submit_survey_error` | Submit fails | status, message |
29
+ | `page_time_spent` | Leaving a content page | pageIndex, durationMs |
30
+ | `optional_question_skipped` | Optional question left blank on forward nav | questionId, pageIndex |
31
+ | `backtrack_revisited` | User returns to earlier page and changes answers | fromPage, toPage, changedAnswers |
32
+ | `question_viewed` | After RECORD_QUESTION_VIEW deduped | questionId |
33
+ | `display_logic_matched` | Display condition newly true | questionId, logic details |
34
+ | `answer_logic_matched` | Answer-logic sub-entity newly visible | entity scope |
35
+ | `question_variant_matched` | Variant text switches | variant id |
36
+ | `end_page_logic_matched` | Resolved end page changes | endPageId |
37
+ | `skip_logic_triggered` | Skip logic newly activates | skip target |
38
+
39
+ ---
40
+
41
+ ## Post-submit rollups
42
+
43
+ | Event | When | Contents |
44
+ |-------|------|----------|
45
+ | `survey_session_summary` | Successful submit | `completed`, `maxVisitedPageIndex`, `totalDurationMs`, question counts (total/answered/skipped), `validationErrorsCount`, `backtrackCount`, `validationHistory` |
46
+ | `logic_session_summary` | Successful submit | Per-logic-kind counts, viewed vs on-path-not-viewed, final end page, `logicEventTimeline` audit (console.table) |
47
+
48
+ Enable `debug: true` in hook options for additional `[SDK:INFO]` transition logs (display/answer/variant/end/skip matches on answer change).
49
+
50
+ ---
51
+
52
+ ## Validation error tracking
53
+
54
+ SDK increments validation error counts and records per-page error history in session summary. Client displays `state.validationErrors[questionId]` banners and uses `id={question.id}` for scroll-to-error.
@@ -61,7 +61,7 @@ Reference implementation: `apps/client/src/components/SurveyPage.tsx`.
61
61
  | Per-type integration | Read [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) before building scales | Answer shapes, visibility helpers, performance notes per type |
62
62
  | Validation scroll | `id={question.id}` on question wrapper | Scroll-to-first-error on failed NEXT/SUBMIT |
63
63
 
64
- Deep dives: [`useSurveySDK.md`](useSurveySDK.md) · [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) · [`placeholders-and-tokens.md`](placeholders-and-tokens.md) · [`partial-save-and-recovery.md`](partial-save-and-recovery.md) · [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md) · [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) · [`display-logic-and-navigation.md`](display-logic-and-navigation.md) · [`question-display-variants.md`](question-display-variants.md) · [`question-numbering.md`](question-numbering.md) · [`end-page-logic.md`](end-page-logic.md) · [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) · [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) · [`progress.md`](progress.md) · [`constraints.md`](constraints.md)
64
+ Deep dives: [`useSurveySDK.md`](useSurveySDK.md) · [`question-type-sdk-matrix.md`](question-type-sdk-matrix.md) · [`placeholders-and-tokens.md`](placeholders-and-tokens.md) · [`partial-save-and-recovery.md`](partial-save-and-recovery.md) · [`survey-lifecycle-analytics.md`](survey-lifecycle-analytics.md) · [`analytics-events-catalog.md`](analytics-events-catalog.md) · [`logic-fields-catalog.md`](logic-fields-catalog.md) · [`file-upload-aws.md`](file-upload-aws.md) · [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) · [`display-logic-and-navigation.md`](display-logic-and-navigation.md) · [`question-display-variants.md`](question-display-variants.md) · [`question-numbering.md`](question-numbering.md) · [`end-page-logic.md`](end-page-logic.md) · [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) · [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md) · [`progress.md`](progress.md) · [`constraints.md`](constraints.md)
65
65
 
66
66
  ---
67
67
 
@@ -12,6 +12,16 @@ From `survey.pages`, collect unique `question.type` values — see `MANIFEST.jso
12
12
 
13
13
  ## Step 2: Create Required Files
14
14
 
15
+ **Copy portable templates from [`templates/`](../templates/) when available** — then customize branding. Otherwise implement from ui-spec Implementation Recipe.
16
+
17
+ | Portable template | Copy to |
18
+ |-------------------|---------|
19
+ | `templates/Question.tsx` | `src/components/Question.tsx` |
20
+ | `templates/RankOrderScale.tsx` | `src/components/RankOrderScale.tsx` |
21
+ | `templates/MatrixDropdown.tsx` | `src/components/MatrixDropdown.tsx` |
22
+ | `templates/CustomSliderTrack.tsx` + `surveyUiIcons.tsx` | same folder |
23
+ | `templates/heatmapCoords.ts` | `src/lib/heatmapCoords.ts` |
24
+
15
25
  | If survey contains | Agent MUST create before `npm run build` | UI spec |
16
26
  |--------------------|------------------------------------------|---------|
17
27
  | `NPS_SCALE` | `RatingScale.tsx` | `03-ui-specs/01-rating.md` |
@@ -46,4 +46,19 @@ Display logic may gate questions on **custom field** values (`conditionEntity.en
46
46
 
47
47
  Alternate question wording (`questionDisplayVariants`) uses the same condition engine but **does not hide** the question — the SDK resolves `questionText` on `state.currentQuestions`. See [`question-display-variants.md`](question-display-variants.md).
48
48
 
49
- See also: [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md), [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md), [`progress.md`](progress.md).
49
+ See also: [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md), [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md), [`progress.md`](progress.md), [`logic-fields-catalog.md`](logic-fields-catalog.md).
50
+
51
+ ---
52
+
53
+ ## Worked example — dynamic display on same page
54
+
55
+ **Setup:** Page 1 has Q1 (MCQ trigger) and Q2 (display-gated, `isDynamicAppearanceEnabled: true`). Q2 hidden until Q1 selects option A.
56
+
57
+ 1. User loads page → `currentQuestions` = [Q1] only; progress denominator excludes Q2.
58
+ 2. User selects A → SDK evaluates display logic → `currentQuestions` = [Q1, Q2]; Q2 animates in; progress total increases.
59
+ 3. User answers Q2 → normal CHANGE flow.
60
+ 4. User changes Q1 away from A → SDK sanitizes Q2 answer; Q2 hidden again; progress credits adjust.
61
+
62
+ **Client:** render `state.currentQuestions` only — no manual show/hide. Re-run IntersectionObserver effect when `currentQuestions` changes.
63
+
64
+ **Back navigation:** if display logic hides all questions on a page, SDK skips empty pages — user never sees a blank screen (see skip/display integration tests).
@@ -0,0 +1,116 @@
1
+ # File Upload — Presigned S3 Flow
2
+
3
+ > Required for **submittable** file-upload answers. Raw `File[]` in `state.answers` is **dropped** at submit — SDK expects `UploadedFileAnswer[]` with `id` and `mediaUrl`.
4
+
5
+ Related: [`03-ui-specs/10-file-upload.md`](../03-ui-specs/10-file-upload.md) · [`02-reference/question-types/11-file-upload.md`](../02-reference/question-types/11-file-upload.md)
6
+
7
+ ---
8
+
9
+ ## Answer contract
10
+
11
+ ```typescript
12
+ type UploadedFileAnswer = {
13
+ id: string;
14
+ mediaUrl: string;
15
+ name?: string;
16
+ mimeType?: string;
17
+ size?: number;
18
+ };
19
+
20
+ onAction({
21
+ type: 'CHANGE',
22
+ payload: { questionId, answerValue: UploadedFileAnswer[] },
23
+ });
24
+ ```
25
+
26
+ Submit formatter maps to `quesIdVsAttachmentDetails[questionId]`.
27
+
28
+ ---
29
+
30
+ ## Upload API (EC2 reference — Day 22)
31
+
32
+ **Base URL:** configure via env (example: `http://43.204.26.213:3000` or project-specific upload service).
33
+
34
+ ### Step 1 — Request presigned URL
35
+
36
+ ```http
37
+ POST /get-upload-url
38
+ Content-Type: application/json
39
+
40
+ {
41
+ "fileName": "report.pdf",
42
+ "contentType": "application/pdf",
43
+ "questionId": "<question.id>"
44
+ }
45
+ ```
46
+
47
+ **Response:**
48
+
49
+ ```json
50
+ {
51
+ "uploadUrl": "https://s3.amazonaws.com/...",
52
+ "fileId": "generated-id",
53
+ "mediaUrl": "https://cdn.../path/to/file"
54
+ }
55
+ ```
56
+
57
+ ### Step 2 — PUT file to S3
58
+
59
+ ```http
60
+ PUT {uploadUrl}
61
+ Content-Type: application/pdf
62
+ Body: <file binary>
63
+ ```
64
+
65
+ ### Step 3 — Update SDK state
66
+
67
+ Append to answer array and call `onAction(CHANGE)` with `{ id: fileId, mediaUrl, name, mimeType, size }`.
68
+
69
+ ---
70
+
71
+ ## Client implementation sketch
72
+
73
+ ```typescript
74
+ async function uploadFile(file: File, questionId: string): Promise<UploadedFileAnswer> {
75
+ const res = await fetch(`${UPLOAD_API_BASE}/get-upload-url`, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify({
79
+ fileName: file.name,
80
+ contentType: file.type,
81
+ questionId,
82
+ }),
83
+ });
84
+ const { uploadUrl, fileId, mediaUrl } = await res.json();
85
+ await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
86
+ return { id: fileId, mediaUrl, name: file.name, mimeType: file.type, size: file.size };
87
+ }
88
+ ```
89
+
90
+ Wire `UPLOAD_API_BASE` from env — **never** embed AWS credentials in client code.
91
+
92
+ ---
93
+
94
+ ## UI states
95
+
96
+ | State | UX |
97
+ |-------|-----|
98
+ | Uploading | Spinner on file row; disable dropzone |
99
+ | Success | Show file in list; call `onAction(CHANGE)` |
100
+ | Failure | Red banner; keep local file uncommitted |
101
+ | Remove | Update array; `undefined` when empty |
102
+
103
+ ---
104
+
105
+ ## Partial save
106
+
107
+ Only call `onAction(CHANGE)` after upload completes — partial save should not POST raw `File` blobs.
108
+
109
+ ---
110
+
111
+ ## Agent checklist
112
+
113
+ - [ ] Local validation before upload (format, count, size)
114
+ - [ ] Presigned PUT wired per file
115
+ - [ ] Final answer is `UploadedFileAnswer[]` with `id` + `mediaUrl`
116
+ - [ ] Submit body includes attachments (verify in network tab)
@@ -0,0 +1,105 @@
1
+ # Logic Fields Catalog
2
+
3
+ > Central reference for skip, display, answer, variant, and end-page logic fields by question type. Agents do **not** implement evaluators — wire UI + hook; SDK evaluates conditions.
4
+
5
+ Cross-links: [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md) · [`display-logic-and-navigation.md`](display-logic-and-navigation.md) · [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md) · [`end-page-logic.md`](end-page-logic.md)
6
+
7
+ ---
8
+
9
+ ## MCQ
10
+
11
+ | Field | Used in | Operators | Notes |
12
+ |-------|---------|-----------|-------|
13
+ | `SELECTED_OPTIONS` / option id | skip, display, end | contains, does not contain, equals | Single or multi option ids |
14
+ | `NUM_OF_SELECTED_OPTIONS` | skip, display, end | `EQUALS`, `GT`, `LT`, etc. | Count of selected option ids; zero = missing for EXISTS |
15
+ | Option-level answer logic | answer (UI) | same engine | `getVisibleMcqOptionsForAnswers` |
16
+
17
+ ---
18
+
19
+ ## TEXTFIELD / TEXT_AND_MEDIA
20
+
21
+ | Field | Used in | Operators |
22
+ |-------|---------|-----------|
23
+ | `TEXT` / `CASE_SENSITIVE_TEXT` | skip, display, end | empty, contains, equals, etc. |
24
+ | TEXT_AND_MEDIA | — | no answer stored |
25
+
26
+ ---
27
+
28
+ ## NPS_SCALE
29
+
30
+ | Field | Used in | Operators |
31
+ |-------|---------|-----------|
32
+ | `TEN_POINT_SCALE` / scale value | skip, display, end | numeric compare `<`, `>`, `==`, `!=` |
33
+ | Scale point answer logic | answer (UI) | `getVisibleScaleColumnsForAnswers` |
34
+
35
+ ---
36
+
37
+ ## CFM_MATRIX / CSAT_MATRIX / RATING_MATRIX
38
+
39
+ | Field | Used in | Operators |
40
+ |-------|---------|-----------|
41
+ | Per-row composite keys | skip, display | per statement row + column id |
42
+ | Matrix cell values | skip, display | numeric or column id match |
43
+ | Row/column answer logic | answer (UI) | `getVisibleMatrixItemsForAnswers` |
44
+
45
+ Store answers as column **`id`** via `matrixColumnStoredValue`.
46
+
47
+ ---
48
+
49
+ ## SLIDER_MATRIX
50
+
51
+ | Field | Used in | Operators |
52
+ |-------|---------|-----------|
53
+ | Row statement values | skip, display | numeric compare |
54
+ | Row answer logic | answer (UI) | `getVisibleStatementRowsForAnswers` |
55
+
56
+ ---
57
+
58
+ ## FILE_UPLOAD
59
+
60
+ | Field | Used in | Operators |
61
+ |-------|---------|-----------|
62
+ | Presence only | skip, display | `EXISTS`, `MISSING` |
63
+ | Evaluation | on page entry | no discrete value-change events |
64
+
65
+ ---
66
+
67
+ ## HEATMAP
68
+
69
+ | Field | Used in | Operators |
70
+ |-------|---------|-----------|
71
+ | `HEATMAP` | skip, display | spot count, presence |
72
+ | `HEATMAP_REGION` | skip, display | region label match |
73
+
74
+ Regions are **logic-only** — not rendered on respondent UI.
75
+
76
+ ---
77
+
78
+ ## RANK_ORDER
79
+
80
+ | Field | Used in | Operators |
81
+ |-------|---------|-----------|
82
+ | `RANK_ORDER` (per option) | skip, display, answer | ranked as (`EQUALS`), ranked more/less than (`GT`/`LT`), ranked above/below (`BEFORE`/`AFTER`) |
83
+ | Whole question | skip, display | `EXISTS`, `MISSING` |
84
+
85
+ See [`02-reference/question-types/13-rank-order.md`](../02-reference/question-types/13-rank-order.md).
86
+
87
+ ---
88
+
89
+ ## CUSTOM_FIELD (all types)
90
+
91
+ | Field | Used in | Value shapes |
92
+ |-------|---------|--------------|
93
+ | `_c_*` entity ids | skip, display, end, answer | TEXT, NUMBER, PICKLIST, DATETIME, multiselect |
94
+
95
+ Pass stable `customFieldValues` to hook **and** visibility helpers. See [`custom-field-logic-and-navigation.md`](custom-field-logic-and-navigation.md).
96
+
97
+ ---
98
+
99
+ ## Client wiring summary
100
+
101
+ | Logic kind | Client action |
102
+ |------------|---------------|
103
+ | Skip / display / end / variants | Render `state.currentQuestions`; no client evaluators |
104
+ | Answer logic | `getVisible*ForAnswers(..., customFieldValues?)` before render |
105
+ | Custom fields | Two wiring points: hook options + Question visibility helpers |
@@ -63,6 +63,30 @@ Do not block navigation (NEXT/PREVIOUS) on partial save failure unless product e
63
63
 
64
64
  ---
65
65
 
66
+ ## Per-type debounce guidance
67
+
68
+ | Question type | Client UI note |
69
+ |---------------|----------------|
70
+ | TEXTFIELD (long) | Debounce `onAction(CHANGE)` 300–500 ms to reduce partial-save churn |
71
+ | SLIDER_MATRIX / graphics | Debounce slider `onChange` or rely on SDK debounce |
72
+ | FILE_UPLOAD | Call `CHANGE` only **after** presigned upload completes — see [`file-upload-aws.md`](file-upload-aws.md) |
73
+ | MCQ / matrix | Immediate `CHANGE` on selection is fine |
74
+ | TEXT_AND_MEDIA | No CHANGE — excluded from partial save payload |
75
+
76
+ ---
77
+
78
+ ## Recovery (honest scope)
79
+
80
+ Reload behavior depends on **backend** + instance configuration — the SDK debounces POST to `responseV2` with `surveySubmitted: false` but does not implement client-side resume UI. If product requires resume:
81
+
82
+ - Enable `savePartialResponse`
83
+ - Optionally show `partialSaveResults` status
84
+ - Confirm with backend team whether refetch restores `state.answers`
85
+
86
+ Path-change sanitization (skip/display) clears off-path answers before partial save — see [`skip-logic-and-navigation.md`](skip-logic-and-navigation.md).
87
+
88
+ ---
89
+
66
90
  ## Verify checklist
67
91
 
68
92
  - [ ] `savePartialResponse` only set when required
@@ -38,7 +38,9 @@ Legend: **opt** = option-level; **row+col** = statement rows and scale columns;
38
38
  | **Value derivation** | Store `option.id`; SDK maps to API value at submit |
39
39
  | **Performance notes** | Filter options once per render via helper; avoid re-fetch on option click |
40
40
  | **Docs to read** | [`02-question.md`](../01-components/02-question.md) · [`question-types/README.md#mcq`](../02-reference/question-types/README.md#mcq) · [`02-radio.md`](../03-ui-specs/02-radio.md) |
41
- | **Known limitations** | Multi-select array shape is client responsibility; verify `selectionMode` from survey config |
41
+ | **Default answers** | SDK `applyMcqDefaults` seeds `state.answers` from `defaultOptionIds` on load — client binds `selectedValue` only |
42
+ | **Answer conditions** | SDK validates `minSelections` / `maxSelections` on NEXT; client blocks add at max + shows `validationError` |
43
+ | **Known limitations** | Multi-select stores `string[]`; use `selectionMode` not `isMultiSelect` |
42
44
 
43
45
  ---
44
46
 
@@ -160,18 +162,18 @@ Legend: **opt** = option-level; **row+col** = statement rows and scale columns;
160
162
 
161
163
  | Column | Value |
162
164
  |--------|-------|
163
- | **SDK type + answer shape** | `FileUploadQuestion`; answer `UploadedFile[]` |
165
+ | **SDK type + answer shape** | `FileUploadQuestion`; answer `UploadedFileAnswer[]` (`{ id, mediaUrl, name?, mimeType?, size? }`) |
164
166
  | **Component file(s)** | `FileUploadScale.tsx` |
165
167
  | **Dispatcher branch** | `QUESTION_TYPE.FILE_UPLOAD` / `'FILE_UPLOAD'` |
166
168
  | **Visibility helper** | None |
167
169
  | **Answer logic scope** | None |
168
170
  | **Skip / display / variants / numbering** | SDK auto |
169
171
  | **Custom field logic** | Skip/display only |
170
- | **`onAction` pattern** | `{ type: 'CHANGE', payload: { questionId, answerValue: UploadedFile[] } }` after upload completes |
171
- | **Value derivation** | SDK submit maps file metadata to API |
172
- | **Performance notes** | Upload to presigned URL before CHANGE; show progress UI |
173
- | **Docs to read** | [`08-file-upload-scale.md`](../01-components/08-file-upload-scale.md) · [`11-file-upload.md`](../02-reference/question-types/11-file-upload.md) · [`10-file-upload.md`](../03-ui-specs/10-file-upload.md) |
174
- | **Known limitations** | Blocking question validate file count/size client-side per config |
172
+ | **`onAction` pattern** | `{ type: 'CHANGE', payload: { questionId, answerValue: UploadedFileAnswer[] } }` **after** presigned upload completes |
173
+ | **Value derivation** | SDK submit maps to `quesIdVsAttachmentDetails` raw `File[]` is **not submitted** |
174
+ | **Performance notes** | Local validate → POST presigned URL PUT file → then CHANGE; show progress UI |
175
+ | **Docs to read** | [`file-upload-aws.md`](file-upload-aws.md) · [`08-file-upload-scale.md`](../01-components/08-file-upload-scale.md) · [`11-file-upload.md`](../02-reference/question-types/11-file-upload.md) · [`10-file-upload.md`](../03-ui-specs/10-file-upload.md) |
176
+ | **Known limitations** | Must wire upload API; see [`file-upload-aws.md`](file-upload-aws.md) |
175
177
 
176
178
  ---
177
179
 
@@ -198,14 +200,14 @@ Legend: **opt** = option-level; **row+col** = statement rows and scale columns;
198
200
 
199
201
  | Column | Value |
200
202
  |--------|-------|
201
- | **SDK type + answer shape** | `HeatmapQuestion`; answer `HeatmapAnswer` (`{ clicks: HeatmapClickPoint[] }`) |
203
+ | **SDK type + answer shape** | `HeatmapQuestion`; answer `HeatmapClickPoint[]` (array of `{ id, x, y }` normalized 0–1) |
202
204
  | **Component file(s)** | `HeatmapScale.tsx` + portable coord helper (see [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts)) |
203
205
  | **Dispatcher branch** | `QUESTION_TYPE.HEATMAP` / `'HEATMAP'` |
204
206
  | **Visibility helper** | None |
205
207
  | **Answer logic scope** | None |
206
208
  | **Skip / display / variants / numbering** | SDK auto |
207
209
  | **Custom field logic** | Skip/display only |
208
- | **`onAction` pattern** | `{ type: 'CHANGE', payload: { questionId, answerValue: HeatmapAnswer } }` on each click add/remove |
210
+ | **`onAction` pattern** | `{ type: 'CHANGE', payload: { questionId, answerValue: HeatmapClickPoint[] \| undefined } }` on each click add/remove |
209
211
  | **Value derivation** | Normalized 0–1 coordinates; respect `maxClicksAllowed` |
210
212
  | **Performance notes** | Normalize coords on image load; use `findSpotNearClick` for hit testing |
211
213
  | **Docs to read** | [`17-heatmap-scale.md`](../01-components/17-heatmap-scale.md) · [`12-heatmap.md`](../02-reference/question-types/12-heatmap.md) · [`13-heatmap.md`](../03-ui-specs/13-heatmap.md) |
@@ -138,15 +138,15 @@ For **existing apps**, read `package.json` and project structure — do not scaf
138
138
 
139
139
  After fetching the survey, install extra packages only when those types appear:
140
140
 
141
- ### RANK_ORDER — drag-and-drop mode
141
+ ### RANK_ORDER — dnd-kit (required when survey contains RANK_ORDER)
142
142
 
143
- When the survey contains `RANK_ORDER` with `interactionMode: 'dragAndDrop'` (or you implement drag UI):
143
+ Install whenever the survey inventory includes **`RANK_ORDER`** the portable template supports both dropdown and drag-and-drop modes:
144
144
 
145
145
  ```bash
146
146
  npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
147
147
  ```
148
148
 
149
- See [`18-rank-order-scale.md`](../01-components/18-rank-order-scale.md) and [`question-type-sdk-matrix.md#rank_order`](question-type-sdk-matrix.md#rank_order).
149
+ Copy [`templates/RankOrderScale.tsx`](../templates/RankOrderScale.tsx) before building. See [`18-rank-order-scale.md`](../01-components/18-rank-order-scale.md).
150
150
 
151
151
  ### HEATMAP — coordinate normalization
152
152
 
@@ -77,4 +77,4 @@ Event: `optional_question_skipped` with `questionId` and `pageIndex`.
77
77
  - Use `state.canGoBack` for the Back button (not `currentPageIndex === 0`).
78
78
  - Use `state.primaryButtonType` for Next vs Submit label; SDK maps `NEXT` → `SUBMIT` on the last page when appropriate.
79
79
 
80
- See also: [`progress.md`](progress.md), [`useSurveySDK.md`](useSurveySDK.md), [`display-logic-and-navigation.md`](display-logic-and-navigation.md), [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md).
80
+ See also: [`progress.md`](progress.md), [`useSurveySDK.md`](useSurveySDK.md), [`display-logic-and-navigation.md`](display-logic-and-navigation.md), [`answer-logic-and-navigation.md`](answer-logic-and-navigation.md), [`logic-fields-catalog.md`](logic-fields-catalog.md).
@@ -79,7 +79,7 @@ useEffect(() => {
79
79
 
80
80
  ## Debug: `logic_session_summary`
81
81
 
82
- When `debug: true` is set in hook options, the SDK emits structured console events including lifecycle transitions. Cross-reference event names in [`useSurveySDK.md`](useSurveySDK.md) during integration testing.
82
+ When `debug: true` is set in hook options, the SDK emits structured console events including lifecycle transitions. Full event catalog: [`analytics-events-catalog.md`](analytics-events-catalog.md).
83
83
 
84
84
  ---
85
85
 
@@ -23,9 +23,10 @@ If `true`, the standard Row-by-Column matrix is inverted:
23
23
  - **Architectural Requirement**: Your `<tbody>` map must iterate over `question.columns` first, and render `<td>` cells mapped from `question.rows`.
24
24
  - Ensure `onSelect` still correctly pairs the original `row.id` with the original `col.value`.
25
25
 
26
- ### 2. `repeatScale`
26
+ ### 2. `repeatColumnHeaders`
27
27
  If `true`, the `<thead>` containing column labels must be re-injected periodically to prevent users from losing track of columns on long matrices.
28
28
  - **Implementation Strategy**: In your row `map()`, if `index > 0 && index % 5 === 0`, return a React Fragment containing a cloned `<tr>` header row followed by the actual data `<tr>`.
29
+ - **Note:** API/config field is `repeatColumnHeaders` (not `repeatScale`).
29
30
 
30
31
  ### 3. `matrixType === 'bipolar'`
31
32
  Standard matrices (`'likert'`) only show statement text on the left. Bipolar scales show text on BOTH sides of the radio buttons.
@@ -1,67 +1,30 @@
1
1
  # File Upload Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/10-file-upload.md`
4
- > **Reference implementation:** `apps/client/src/components/FileUploadScale.tsx`
5
- > **Target Component**: `FileUploadScale`
6
- > **Handles**: `type: 'FILE_UPLOAD'`
4
+ > **Upload API:** [`00-integration/file-upload-aws.md`](../00-integration/file-upload-aws.md)
5
+ > **Portable template:** implement per ui-spec (upload wiring is project-specific)
6
+ > **Target Component:** `FileUploadScale`
7
+ > **Handles:** `type: 'FILE_UPLOAD'`
7
8
 
8
9
  ## Core Responsibility
9
- Render a robust drag-and-drop zone and enforce complex file size, type limits, and maximum file count constraints BEFORE updating the state.
10
10
 
11
- ## State Management
12
- - `selectedValue`: Expects an array of native `File` objects (or serialized equivalents).
13
- - **Mutation Pattern**:
14
- 1. The user selects files.
15
- 2. The component intercepts the files.
16
- 3. The component validates them against ALL limits.
17
- 4. If valid, append the new files to `selectedValue` (handling it as an array).
18
- 5. Call `onSelect(updatedFilesArray)`.
19
-
20
- ## Configuration Matrix (CRITICAL FOR PLANNING)
21
-
22
- Before calling `onSelect`, your component MUST validate against these properties:
23
-
24
- ### 1. `supportedFileFormats`
25
- - Extracted from `question.supportedFileFormats` (e.g. `['PDF', 'PNG', 'JPG']`).
26
- - Use this to build the `accept` attribute string for the hidden input: `question.supportedFileFormats?.map(f => \`.\${f.toLowerCase()}\`).join(',')`.
27
- - You MUST reject files with extensions that are not in this list.
11
+ Render drag-and-drop zone, validate files locally, **upload via presigned URL**, then bubble `UploadedFileAnswer[]` via `onSelect`.
28
12
 
29
- ### 2. `maxFileCount`
30
- - Prevent uploads if `currentFiles.length + newFiles.length > maxFileCount`.
31
- - Provide a clear local error message indicating the max limit.
32
-
33
- ### 3. `fileSizeLimit` & `fileSizeLimitType`
34
- - `fileSizeLimit` is a number representing megabytes (MB).
35
- - If `fileSizeLimitType === 'PER_FILE'`, validate that EVERY incoming file's `size` (in bytes) is `<= fileSizeLimit * 1024 * 1024`.
36
- - If `fileSizeLimitType === 'IN_TOTAL'`, validate that the combined sum of all existing `selectedValue` sizes plus the incoming files' sizes is `<= fileSizeLimit * 1024 * 1024`.
37
-
38
- ## UI/UX Specifications
39
-
40
- 1. **Dropzone**:
41
- - Wrap the main area with `onDragEnter`, `onDragOver`, `onDragLeave`, `onDrop`.
42
- - **Inactive State**: `border-2 border-dashed border-gray-300 rounded-xl bg-gray-50 p-8 text-center cursor-pointer transition-all`.
43
- - **Drag/Hover State**: `border-[#e20074] bg-[#fdf2f8]` (Using the brand primary color).
44
- - Display a Cloud SVG icon (use gray, switch to magenta on hover).
45
- - Render `question.uploadMessage` (which can contain HTML, so use `dangerouslySetInnerHTML`).
46
- - Dynamically render the limits text (e.g., "Max 3 files, 10MB per file").
13
+ ## State Management
47
14
 
48
- 2. **File List**:
49
- - Render existing files as distinct cards underneath the dropzone.
50
- - Styling: `flex items-center justify-between p-3 mt-3 border border-gray-200 rounded-lg bg-white shadow-sm`.
51
- - Display the filename (truncated if long) and the formatted file size (e.g., "1.2 MB").
52
- - Include a distinct, circular Trash/Remove button that removes the file from the state and calls `onSelect` with the updated array.
15
+ - `selectedValue`: `UploadedFileAnswer[] | undefined` — **not** raw `File[]` after upload completes
16
+ - **Mutation pattern:**
17
+ 1. User selects files local validation
18
+ 2. For each file: request presigned URL PUT to S3 append `{ id, mediaUrl, name, mimeType, size }`
19
+ 3. `onSelect(uploadedAnswers)` `onAction(CHANGE)`
53
20
 
54
- 3. **Validation Errors**:
55
- - Local validation errors (e.g. "File too large") must be displayed in a highly visible red banner (`bg-red-50 text-red-700 p-3 rounded-md`) right below the dropzone.
21
+ ## Local validation (before upload)
56
22
 
57
- ## Hidden Input
58
- - Include a hidden `<input type="file" multiple={question.maxFileCount > 1} accept={...} />` that is triggered via a `useRef` when the dropzone is clicked.
23
+ Same as ui-spec: `supportedFileFormats`, `maxFileCount`, `fileSizeLimit` + `fileSizeLimitType`.
59
24
 
60
25
  ## SDK Integration Checklist (agent)
61
26
 
62
27
  - [ ] Type literal: `QUESTION_TYPE.FILE_UPLOAD`
63
- - [ ] Answer stored as: `UploadedFile[]`
64
- - [ ] onAction CHANGE: `{ questionId, answerValue: UploadedFile[] }` after upload
65
- - [ ] Visibility helper: N/A
66
- - [ ] Enforce `maxFileCount` and `fileSizeLimit` client-side
28
+ - [ ] Final answer: `UploadedFileAnswer[]` with `id` + `mediaUrl`
29
+ - [ ] Wire upload API per [`file-upload-aws.md`](../00-integration/file-upload-aws.md)
67
30
  - [ ] Matrix row: [`question-type-sdk-matrix.md#file_upload`](../00-integration/question-type-sdk-matrix.md#file_upload)