@explorer02/cfm-survey-sdk 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@explorer02/cfm-survey-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
package/postinstall.js CHANGED
@@ -53,8 +53,13 @@ Do NOT run `cfm-sdk init`, read `survey-config.json`, or use any setup wizard.
53
53
 
54
54
  RULE 2 — DEEP CONTEXT BEFORE CODE:
55
55
  Before writing React code, read EVERY file in `node_modules/@explorer02/cfm-survey-sdk/docs/01-components/`.
56
+ Read `docs/00-integration/component-checklist.md` and `docs/03-ui-specs/` for EVERY question type in the fetched survey (blocking for matrix, slider_matrix, file_upload).
57
+ Copy `docs/templates/Question.tsx` as your Question dispatcher — do NOT use stub/default fallback branches.
56
58
  Copy `node_modules/@explorer02/cfm-survey-sdk/docs/templates/implementation_plan.md` to project root and fill it in.
57
59
 
60
+ RULE 2b — NO STUB PLACEHOLDERS:
61
+ Never render "not yet implemented" or similar placeholder text. If a type is in the survey, implement its scale component.
62
+
58
63
  RULE 3 — AUTO-PROCEED:
59
64
  After the implementation plan is complete, proceed to build without waiting for approval.
60
65
  The workflow requires 3 client inputs: Prompt/Mockup, Instance ID (JWT), Deploy Confirmation.
@@ -21,11 +21,13 @@ Import from whatever appears in `package.json`:
21
21
  | 1 | Scaffold React app — `00-integration/setup.md` |
22
22
  | 2 | Ask client for instanceId JWT |
23
23
  | 3 | Read `useSurveySDK.md` — hook contract |
24
- | 4 | Read all `01-components/`; copy `templates/implementation_plan.md` → project root |
25
- | 5 | Build UI per blueprints + `constraints.md` |
24
+ | 4 | Read all `01-components/`; inventory survey types; read `03-ui-specs/` per type; copy `templates/Question.tsx` + `implementation_plan.md` |
25
+ | 5 | Build UI create all scale files from `component-checklist.md`; no stub placeholders |
26
26
  | 6 | Verify build checklist |
27
27
  | 7 | Deploy on client confirmation |
28
28
 
29
29
  **Do NOT** run `cfm-sdk init`, read `survey-config.json`, or use any setup wizard.
30
30
 
31
+ **Do NOT** render `"not yet implemented"` or default fallback stubs in `Question.tsx`. Copy `docs/templates/Question.tsx` and create all required scale components.
32
+
31
33
  Follow the master index strictly. Do not guess SDK internals. Do not import from `/src/` paths.
@@ -0,0 +1,61 @@
1
+ # Required Component Files Checklist
2
+
3
+ > **Blocking gate before Phase 5 (BUILD).** After fetching the survey with `instanceId`, list every `(type, subType)` pair and ensure all required files exist.
4
+
5
+ ## Step 1: Inventory Question Types
6
+
7
+ From `survey.pages`, collect unique combinations:
8
+
9
+ | questionId | type | subType (if matrix) |
10
+ |------------|------|---------------------|
11
+ | | | |
12
+
13
+ ## Step 2: Create Required Files
14
+
15
+ | If survey contains | Agent MUST create before `npm run build` | UI spec |
16
+ |--------------------|------------------------------------------|---------|
17
+ | `rating` | `RatingScale.tsx` | `03-ui-specs/01-rating.md` |
18
+ | `csat` | `CsatScale.tsx` | `03-ui-specs/04-csat.md` |
19
+ | `rating_scale` | Reuse `CsatScale.tsx` or dedicated component | `03-ui-specs/05-rating-scale.md` |
20
+ | `slider` | `CustomSliderTrack.tsx` + wrapper | `03-ui-specs/06-slider.md` |
21
+ | `matrix` + `CFM_MATRIX` | `LikertMatrixScale.tsx`, `MatrixDropdown.tsx` | `03-ui-specs/07-matrix-cfm.md` |
22
+ | `matrix` + `CSAT_MATRIX` or `RATING_MATRIX` | `CsatMatrixScale.tsx`, `MatrixDropdown.tsx`, `CustomSliderTrack.tsx` | `03-ui-specs/08-matrix-csat-rating.md` |
23
+ | `slider_matrix` | `SliderMatrixScale.tsx`, `CustomSliderTrack.tsx` | `03-ui-specs/09-slider-matrix.md` |
24
+ | `file_upload` | `FileUploadScale.tsx` | `03-ui-specs/10-file-upload.md` |
25
+ | `radio`, `text`, `text_and_media` | Inline in `Question.tsx` | `03-ui-specs/02-radio.md`, `03-text.md`, `11-text-and-media.md` |
26
+
27
+ Always create **`Question.tsx`** from [`templates/Question.tsx`](../templates/Question.tsx).
28
+
29
+ ## Matrix subType Routing (CRITICAL)
30
+
31
+ `question.type === 'matrix'` alone renders **nothing**. You MUST branch on `subType`:
32
+
33
+ ```tsx
34
+ {question.type === 'matrix' && question.subType === 'CFM_MATRIX' && (
35
+ <LikertMatrixScale question={question} selectedValue={...} onSelect={onSelect} />
36
+ )}
37
+
38
+ {question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && (
39
+ <CsatMatrixScale question={question} selectedValue={...} onSelect={onSelect} />
40
+ )}
41
+ ```
42
+
43
+ ## Shared Primitives
44
+
45
+ | File | Used by |
46
+ |------|---------|
47
+ | `CustomSliderTrack.tsx` | `slider_matrix`, CSAT matrix `graphics`, standalone `slider` |
48
+ | `MatrixDropdown.tsx` | `LikertMatrixScale`, `CsatMatrixScale` vertical/dropdown modes |
49
+
50
+ Specs: `03-ui-specs/shared/custom-slider-track.md`, `03-ui-specs/shared/matrix-dropdown.md`
51
+
52
+ ## Step 3: Verify Before Build
53
+
54
+ - [ ] Every type in the survey inventory has a matching component file
55
+ - [ ] `Question.tsx` copied from `docs/templates/Question.tsx` with all branches wired
56
+ - [ ] No `default` fallback rendering stub text (see `constraints.md` §7)
57
+ - [ ] Grep project: `not yet implemented` → **zero matches**
58
+
59
+ ```bash
60
+ grep -r "not yet implemented" src/ components/ app/ 2>/dev/null || true
61
+ ```
@@ -40,6 +40,22 @@ Survey UI files importing the SDK must be client components. Next.js: `'use clie
40
40
 
41
41
  Define `SURVEY_PLACEHOLDERS` outside the component — inline objects cause infinite re-fetch.
42
42
 
43
+ ## 7. No Stub Placeholders (CRITICAL)
44
+
45
+ **FORBIDDEN** in `Question.tsx` or any scale component:
46
+
47
+ - Text like `"not yet implemented"`, `"coming soon"`, or any placeholder error message
48
+ - `default` / `else` branches in the dispatcher that render stub UI instead of the real component
49
+ - Empty `<div>` where an interactive scale component should be
50
+
51
+ If a question type appears in the fetched survey, its component **must** be fully implemented before shipping. Read the matching `03-ui-specs/` doc and create the required files listed in `component-checklist.md`.
52
+
53
+ **Phase 6 verification:** grep the project for stub text — must return zero matches:
54
+
55
+ ```bash
56
+ grep -r "not yet implemented" src/ components/ app/ 2>/dev/null || true
57
+ ```
58
+
43
59
  ---
44
60
 
45
61
  ## Troubleshooting
@@ -52,3 +68,4 @@ Define `SURVEY_PLACEHOLDERS` outside the component — inline objects cause infi
52
68
  | Validation errors but no scroll | Add `id={question.id}` to question wrapper |
53
69
  | Survey data is null, no error | Invalid/expired `instanceId` — ask client for fresh JWT |
54
70
  | CSAT emojis missing | `npm install react-icons` |
71
+ | Grey "not yet implemented" text on questions | Copy `docs/templates/Question.tsx`; create scale components per `component-checklist.md`; route matrix by `subType` |
@@ -1,10 +1,19 @@
1
1
  # Question Dispatcher Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/00-question-shell.md`
4
- > **Reference implementation:** `apps/client/src/components/Question.tsx`
4
+ > **Canonical template:** [`templates/Question.tsx`](../templates/Question.tsx) — copy verbatim, then create imported components
5
+ > **Monorepo reference:** `apps/client/src/components/Question.tsx` (not available in npm installs)
5
6
  > **Target Component**: `Question`
6
7
  > **Role**: Wrapper card component that routes to the correct scale component based on `question.type`.
7
8
 
9
+ ## Start Here (CRITICAL)
10
+
11
+ 1. Copy [`docs/templates/Question.tsx`](../templates/Question.tsx) into your project.
12
+ 2. Create every imported scale component — see [`component-checklist.md`](../00-integration/component-checklist.md).
13
+ 3. Read `03-ui-specs/` for each type in your fetched survey **before** editing the dispatcher.
14
+
15
+ **Do NOT** write a partial skeleton with `default` fallbacks or `"not yet implemented"` messages.
16
+
8
17
  ## Core Responsibility
9
18
  Render a visually separated container (card) for each question. Handle rich text parsing, required asterisks, description text, and error validation messages. Most importantly, it MUST act as an exhaustive switch statement dispatching to ALL supported question types.
10
19
 
@@ -12,17 +21,34 @@ Render a visually separated container (card) for each question. Handle rich text
12
21
 
13
22
  The agent MUST handle EVERY SINGLE ONE of these `question.type` values. Failure to handle them will result in blank UI or broken surveys.
14
23
 
15
- 1. **`rating`**: Renders `RatingScale`. This covers NPS, Stars, Emoji, and generic numeric scales.
24
+ 1. **`rating`**: Renders `RatingScale`. This covers NPS scales.
16
25
  2. **`csat`**: Renders `CsatScale`. This covers CSAT specific faces/icons.
17
- 3. **`radio`**: Renders inline MCQ radio buttons. (Can be implemented inside this file or separately).
26
+ 3. **`radio`**: Renders inline MCQ radio buttons.
18
27
  4. **`text`**: Renders an inline `<textarea>` or `<input>`.
19
- 5. **`matrix`**:
28
+ 5. **`matrix`** — **requires `subType`**:
20
29
  - If `subType === 'CFM_MATRIX'`, render `LikertMatrixScale`
21
30
  - If `subType === 'CSAT_MATRIX'` or `'RATING_MATRIX'`, render `CsatMatrixScale`
31
+ - Routing on `type === 'matrix'` alone renders **nothing**
22
32
  6. **`slider_matrix`**: Renders `SliderMatrixScale`
23
33
  7. **`file_upload`**: Renders `FileUploadScale`
24
- 8. **`text_and_media`**: Renders inline `<video>` or `<img>` based on `question.mediaUrl`.
25
- - *Note: `text_and_media` questions may NOT have user input. Do not render a scale for them.*
34
+ 8. **`text_and_media`**: Renders inline `<video>` or `<img>` based on `question.mediaUrl`. No answer input.
35
+ 9. **`rating_scale`**: Star/emoji scale see `03-ui-specs/05-rating-scale.md`
36
+ 10. **`slider`**: Standalone slider — see `03-ui-specs/06-slider.md`
37
+
38
+ ## Forbidden Anti-Patterns
39
+
40
+ ```tsx
41
+ // ❌ FORBIDDEN — never ship this
42
+ default:
43
+ return <p>Question type "{question.type}" is not yet implemented in this client.</p>;
44
+
45
+ // ❌ FORBIDDEN — matrix without subType
46
+ {question.type === 'matrix' && <SomeMatrix />}
47
+
48
+ // ✅ REQUIRED — matrix with subType
49
+ {question.type === 'matrix' && question.subType === 'CFM_MATRIX' && <LikertMatrixScale ... />}
50
+ {question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && <CsatMatrixScale ... />}
51
+ ```
26
52
 
27
53
  ## Container UI Structure
28
54
 
@@ -47,61 +73,20 @@ Every question MUST be wrapped in a standard HTML `<section>` to visually separa
47
73
 
48
74
  4. **Validation Error Banner**:
49
75
  - If `validationError` prop is passed, render a highly visible banner below the scale.
50
- - Example: Red border-left, red background, bold text.
51
76
 
52
77
  ## Hover Tooltips & Micro-interactions
53
78
  - Focus states on text inputs MUST use the brand primary color (e.g., magenta `#e20074` for Telekom).
54
79
  - Radio button wrappers should have a subtle background hover effect (e.g., `hover:bg-pink-50`).
55
80
 
56
- ## Example Architectural Skeleton
81
+ ## Props Contract
57
82
 
58
- ```tsx
59
- <section id={question.id} className="space-y-6 rounded-xl border border-gray-200 bg-white p-6 md:p-8 shadow-sm transition-all hover:shadow-md">
60
-
61
- {/* HEADING */}
62
- <h2 className="text-lg md:text-xl font-semibold leading-relaxed text-gray-900 flex items-start gap-1">
63
- <span dangerouslySetInnerHTML={{ __html: question.text }} className="prose prose-sm max-w-none" />
64
- {question.required && <span className="text-[#e20074] shrink-0">*</span>}
65
- </h2>
66
-
67
- {/* DESCRIPTION */}
68
- {question.description && (
69
- <div className="text-sm text-gray-600 mt-2 leading-relaxed prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: question.description }} />
70
- )}
71
-
72
- {/* DISPATCHER */}
73
- <div className="pt-4">
74
- {question.type === 'rating' && <RatingScale ... />}
75
- {question.type === 'csat' && <CsatScale ... />}
76
- {question.type === 'slider_matrix' && <SliderMatrixScale ... />}
77
- {question.type === 'file_upload' && <FileUploadScale ... />}
78
- {question.type === 'matrix' && question.subType === 'CFM_MATRIX' && <LikertMatrixScale ... />}
79
- {question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && <CsatMatrixScale ... />}
80
-
81
- {question.type === 'radio' && (
82
- // Inline Radio implementation
83
- // Make sure to match brand hover colors!
84
- )}
85
-
86
- {question.type === 'text' && (
87
- // Inline Textarea implementation
88
- // Handle character limit `question.maxCharacterCount`
89
- )}
90
-
91
- {question.type === 'text_and_media' && (
92
- // Display media (image/video) based on question properties
93
- // This handles CONTENT blocks.
94
- )}
95
- </div>
96
-
97
- {/* ERROR BANNER */}
98
- {validationError && (
99
- <div className="mt-6 rounded-lg border-l-4 border-l-[#e20074] bg-red-50 p-4">
100
- <p className="text-sm font-bold text-gray-900 leading-tight flex items-center gap-2">
101
- <span className="text-[#e20074]">⚠</span> {validationError}
102
- </p>
103
- </div>
104
- )}
105
-
106
- </section>
83
+ ```typescript
84
+ type QuestionProps = {
85
+ question: SurveyQuestion;
86
+ selectedValue?: AnswerValue;
87
+ validationError?: string;
88
+ onSelect: (value: AnswerValue) => void;
89
+ };
107
90
  ```
91
+
92
+ Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, value } })`.
@@ -1,6 +1,8 @@
1
1
  # Client Components Overview
2
2
 
3
3
  > Wiring blueprints for React components. **Authoritative UI details** are in [`03-ui-specs/`](../03-ui-specs/README.md) (reference-client driven).
4
+ > **Required files gate:** [`component-checklist.md`](../00-integration/component-checklist.md)
5
+ > **Dispatcher template:** [`templates/Question.tsx`](../templates/Question.tsx)
4
6
  > This folder documents props, `onSelect` wiring, and dispatcher architecture.
5
7
 
6
8
  ## Component Architecture
@@ -1,8 +1,19 @@
1
1
  # UI Spec: Question Shell (`Question.tsx`)
2
2
 
3
- > **Reference:** `apps/client/src/components/Question.tsx`
3
+ > **Canonical template:** [`templates/Question.tsx`](../templates/Question.tsx) — copy this dispatcher verbatim
4
+ > **Monorepo reference:** `apps/client/src/components/Question.tsx` (not available in npm installs — implement from this spec)
4
5
  > **SDK:** All types extend `QuestionBase` from `fetchSurvey/types/base.ts`
5
6
  > **Component blueprint:** `01-components/02-question.md`
7
+ > **Required files:** `00-integration/component-checklist.md`
8
+
9
+ ## Matrix subType Warning (CRITICAL)
10
+
11
+ Routing on `question.type === 'matrix'` **alone renders nothing**. You MUST branch on `subType`:
12
+
13
+ - `CFM_MATRIX` → `LikertMatrixScale`
14
+ - `CSAT_MATRIX` | `RATING_MATRIX` → `CsatMatrixScale`
15
+
16
+ Never use a `default` fallback that shows `"not yet implemented"` — create the scale components listed in the checklist.
6
17
 
7
18
  ## Config Inventory (QuestionBase)
8
19
 
@@ -84,6 +95,8 @@ Wire `onSelect` → `onAction({ type: 'CHANGE', payload: { questionId, value } }
84
95
 
85
96
  - [ ] `id={question.id}` on outermost `<section>`
86
97
  - [ ] HTML rendering for text and description
87
- - [ ] Exhaustive type switchall 12 types handled or explicitly noted as gap
98
+ - [ ] Copied from `templates/Question.tsx`no stub/default fallback branches
99
+ - [ ] Matrix routes by `subType`, not type alone
100
+ - [ ] `slider_matrix` → `SliderMatrixScale`; `file_upload` → `FileUploadScale`
88
101
  - [ ] Validation error banner below scale
89
102
  - [ ] Rating/csat endpoint labels positioned per formulas above
@@ -1,6 +1,6 @@
1
1
  # UI Spec: CFM Matrix (`type: 'matrix'`, `subType: 'CFM_MATRIX'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/LikertMatrixScale.tsx`
3
+ > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/LikertMatrixScale.tsx`
4
4
  > **SDK type:** `MatrixQuestion` — `fetchSurvey/types/matrix.ts`
5
5
  > **Component blueprint:** `01-components/06-likert-matrix-scale.md`
6
6
 
@@ -1,6 +1,6 @@
1
1
  # UI Spec: CSAT / Rating Matrix (`subType: 'CSAT_MATRIX' | 'RATING_MATRIX'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/CsatMatrixScale.tsx`
3
+ > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/CsatMatrixScale.tsx`
4
4
  > **SDK type:** `MatrixQuestion` — `fetchSurvey/types/matrix.ts`
5
5
  > **Component blueprint:** `01-components/05-csat-matrix-scale.md`
6
6
 
@@ -1,6 +1,6 @@
1
1
  # UI Spec: Slider Matrix (`type: 'slider_matrix'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/SliderMatrixScale.tsx`
3
+ > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/SliderMatrixScale.tsx`
4
4
  > **SDK type:** `SliderMatrixQuestion` — `fetchSurvey/types/sliderMatrix.ts`
5
5
  > **Component blueprint:** `01-components/07-slider-matrix-scale.md`
6
6
  > **Shared:** [`shared/custom-slider-track.md`](shared/custom-slider-track.md)
@@ -1,6 +1,6 @@
1
1
  # UI Spec: File Upload (`type: 'file_upload'`)
2
2
 
3
- > **Reference:** `apps/client/src/components/FileUploadScale.tsx`
3
+ > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/FileUploadScale.tsx`
4
4
  > **SDK type:** `FileUploadQuestion` — `fetchSurvey/types/fileUpload.ts`
5
5
  > **Component blueprint:** `01-components/08-file-upload-scale.md`
6
6
 
@@ -1,7 +1,8 @@
1
1
  # UI Specifications — Reference Implementation Guide
2
2
 
3
- > **Authoritative source:** [`apps/client/src/components/`](../../../../../../apps/client/src/components/) in the monorepo.
4
- > Agents must reproduce this UI when building survey apps. Read the spec for each question type present in the fetched survey.
3
+ > **For npm installs:** implement from these specs + [`templates/Question.tsx`](../templates/Question.tsx). The monorepo path `apps/client/src/components/` is **not** available in client projects.
4
+ > **For monorepo devs:** cross-check against [`apps/client/src/components/`](../../../../../../apps/client/src/components/).
5
+ > Read the spec for **each question type present** in the fetched survey before writing React code.
5
6
 
6
7
  ## How to Read a UI Spec
7
8
 
@@ -1,6 +1,6 @@
1
1
  # UI Spec: Custom Slider Track (shared primitive)
2
2
 
3
- > **Reference:** `apps/client/src/components/CustomSliderTrack.tsx`
3
+ > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/CustomSliderTrack.tsx`
4
4
  > **Used by:** `SliderMatrixScale.tsx`, `CsatMatrixScale` (graphics), standalone `slider` (gap)
5
5
 
6
6
  ## Layer Architecture
@@ -1,6 +1,6 @@
1
1
  # UI Spec: Matrix Dropdown (shared primitive)
2
2
 
3
- > **Reference:** `apps/client/src/components/MatrixDropdown.tsx`
3
+ > **Implement from this spec** (npm installs). Monorepo reference: `apps/client/src/components/MatrixDropdown.tsx`
4
4
  > **Used by:** `LikertMatrixScale`, `CsatMatrixScale` vertical list
5
5
 
6
6
  ## Modes
@@ -33,6 +33,8 @@
33
33
  "id": 4,
34
34
  "name": "planAndIngest",
35
35
  "read": [
36
+ "00-integration/component-checklist.md",
37
+ "03-ui-specs/README.md",
36
38
  "01-components/README.md",
37
39
  "01-components/01-survey-page.md",
38
40
  "01-components/02-question.md",
@@ -49,6 +51,7 @@
49
51
  "01-components/13-matrix-dropdown.md"
50
52
  ],
51
53
  "template": "templates/implementation_plan.md",
54
+ "questionDispatcher": "templates/Question.tsx",
52
55
  "output": "implementation_plan.md"
53
56
  },
54
57
  {
@@ -63,6 +66,8 @@
63
66
  "read": ["00-integration/constraints.md"],
64
67
  "checklist": [
65
68
  "questionDispatcherCoversAll12Types",
69
+ "matrixRoutesBySubType",
70
+ "noStubPlaceholderText",
66
71
  "everyQuestionWrapperHasId",
67
72
  "noInternalSdkImports",
68
73
  "surveyPageFiveStates",
@@ -15,7 +15,8 @@ docs/
15
15
  ├── 02-reference/ ← Routing table + config-field-index (on demand)
16
16
  ├── 03-ui-specs/ ← Authoritative UI specs (reference-client driven)
17
17
  └── templates/
18
- └── implementation_plan.md
18
+ ├── implementation_plan.md
19
+ └── Question.tsx ← Canonical dispatcher (copy before building)
19
20
  ```
20
21
 
21
22
  **Import rule:** Use whatever package name appears in the project's `package.json` (`@explorer02/cfm-survey-sdk` or `@repo/sdk`).
@@ -64,23 +65,24 @@ Optional: read `02-reference/routing-table.md` for type overview.
64
65
  ### PHASE 4: PLAN + INGEST (CRITICAL)
65
66
 
66
67
  1. Read **every** file in `01-components/` (all 14 blueprints)
67
- 2. Copy `docs/templates/implementation_plan.md` `./implementation_plan.md`
68
- 3. Fill every section do **not** invent a different structure
69
- 4. **Auto-proceed to Phase 5** do not wait for approval
70
-
71
- For each question type in the fetched survey, also read matching docs from `02-reference/question-types/` (see `MANIFEST.json` or `02-reference/routing-table.md`).
68
+ 2. Read `00-integration/component-checklist.md` and `03-ui-specs/README.md`
69
+ 3. **Inventory** every `(type, subType)` in `survey.pages` after instanceId fetch
70
+ 4. For **each type in the survey**, read matching `03-ui-specs/` (use `MANIFEST.json` `uiSpecs`) and `02-reference/question-types/` **blocking** for `matrix`, `slider_matrix`, `file_upload`
71
+ 5. Copy `docs/templates/implementation_plan.md` → `./implementation_plan.md` and fill every section
72
+ 6. Copy `docs/templates/Question.tsx` as the dispatcher starting point
73
+ 7. **Auto-proceed to Phase 5** — do not wait for approval
72
74
 
73
75
  ### PHASE 5: BUILD
74
76
 
75
- Read: `00-integration/constraints.md`
77
+ Read: `00-integration/constraints.md`, `00-integration/component-checklist.md`
76
78
 
77
- For each question in `state.currentQuestions`:
79
+ Create all required scale component files **before** wiring `Question.tsx`. For each question in the survey:
78
80
 
79
81
  1. Read `02-reference/question-types/{type}.md` for data shape
80
82
  2. Read `03-ui-specs/{NN}-{type}.md` (use `MANIFEST.json` `uiSpecs`; split matrix by `subType`)
81
83
  3. If `matrixFormat`, `buttonType`, or `sliderType` is present → read `03-ui-specs/shared/` specs
82
84
  4. Cross-check `02-reference/config-field-index.md` for every non-default field
83
- 5. Match reference client behavior in `apps/client/src/components/` when available
85
+ 5. Implement per ui-spec monorepo devs may consult `apps/client/src/components/`; npm installs use ui-specs only
84
86
 
85
87
  Implement components per blueprints + UI specs. Use exported types:
86
88
 
@@ -94,7 +96,10 @@ Read: `00-integration/constraints.md` (troubleshooting)
94
96
 
95
97
  Checklist:
96
98
 
97
- - [ ] `Question.tsx` switch covers all 12 `question.type` values
99
+ - [ ] `Question.tsx` copied from `docs/templates/Question.tsx` — covers all 12 `question.type` values
100
+ - [ ] Matrix routes by `subType` (`CFM_MATRIX` vs `CSAT_MATRIX` / `RATING_MATRIX`)
101
+ - [ ] All scale components from `component-checklist.md` exist for types in survey
102
+ - [ ] **Zero stub text:** `grep -r "not yet implemented" src/ components/ app/` returns no matches
98
103
  - [ ] Every question wrapper has `id={question.id}`
99
104
  - [ ] No imports from `@explorer02/cfm-survey-sdk/src/...`
100
105
  - [ ] `SurveyPage` handles loading / error / empty / submitted / active
@@ -113,8 +118,7 @@ If yes: `npx cfm-sdk deploy` (preferred) or `npx vercel --prod --yes`
113
118
  | When | Read |
114
119
  |------|------|
115
120
  | Always first | `index.md`, `00-integration/setup.md`, `00-integration/useSurveySDK.md`, `00-integration/constraints.md` |
116
- | Before any React code | All `01-components/*.md` |
117
- | On demand | `02-reference/question-types/<type>.md` for types in survey |
118
- | Before building each question | `03-ui-specs/` per `MANIFEST.uiSpecs` |
121
+ | Before any React code | All `01-components/*.md`, `00-integration/component-checklist.md`, `templates/Question.tsx` |
122
+ | After instanceId fetch | `03-ui-specs/` per type in survey (blocking for matrix / slider_matrix / file_upload) |
119
123
  | Config audit | `02-reference/config-field-index.md` |
120
124
  | Tooling | `MANIFEST.json`, `templates/implementation_plan.md` |
@@ -0,0 +1,243 @@
1
+ /**
2
+ * CANONICAL QUESTION DISPATCHER TEMPLATE
3
+ *
4
+ * Agents: copy this file to your project's components folder (e.g. src/components/Question.tsx).
5
+ * Then create EVERY imported scale component listed below — see:
6
+ * docs/00-integration/component-checklist.md
7
+ * docs/03-ui-specs/00-question-shell.md
8
+ *
9
+ * RULES:
10
+ * - Do NOT add a default/fallback branch that renders "not yet implemented" or similar stub text.
11
+ * - matrix MUST route by question.subType — type alone is NOT enough.
12
+ * - If a question type appears in the fetched survey, its branch MUST render a real interactive component.
13
+ *
14
+ * Replace @explorer02/cfm-survey-sdk with your package.json import name if different.
15
+ */
16
+ 'use client';
17
+
18
+ import React from 'react';
19
+ import type { SurveyQuestion, AnswerValue } from '@explorer02/cfm-survey-sdk';
20
+ import { CsatScale } from './CsatScale';
21
+ import { CsatMatrixScale } from './CsatMatrixScale';
22
+ import { SliderMatrixScale } from './SliderMatrixScale';
23
+ import { LikertMatrixScale } from './LikertMatrixScale';
24
+ import { FileUploadScale } from './FileUploadScale';
25
+ import RatingScale from './RatingScale';
26
+ // rating_scale / slider gaps — create these if survey contains those types:
27
+ // import { CustomSliderTrack } from './CustomSliderTrack';
28
+
29
+ type QuestionProps = {
30
+ question: SurveyQuestion;
31
+ selectedValue?: AnswerValue;
32
+ validationError?: string;
33
+ onSelect: (value: AnswerValue) => void;
34
+ };
35
+
36
+ export default function Question({ question, selectedValue, validationError, onSelect }: QuestionProps) {
37
+ return (
38
+ <section id={question.id} className="space-y-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
39
+ <h2 className="text-lg font-medium leading-relaxed text-gray-900">
40
+ <span dangerouslySetInnerHTML={{ __html: question.text }} />
41
+ {question.required && <span className="text-red-500 ml-1">*</span>}
42
+ </h2>
43
+ {question.description && (
44
+ <div
45
+ className="text-sm text-gray-600 mt-2 leading-relaxed"
46
+ dangerouslySetInnerHTML={{ __html: question.description }}
47
+ />
48
+ )}
49
+
50
+ {question.type === 'rating' && (
51
+ <div className="space-y-3">
52
+ {(question.minLabel || question.midLabel || question.maxLabel) && (
53
+ <div className="relative w-full h-6 text-xs font-semibold text-gray-500">
54
+ <span
55
+ className="absolute left-0 top-0 max-w-[30%] text-left leading-tight"
56
+ dangerouslySetInnerHTML={{ __html: question.minLabel ?? '' }}
57
+ />
58
+ {question.midLabel && (
59
+ <span
60
+ className="absolute top-0 -translate-x-1/2 text-center max-w-[40%] leading-tight"
61
+ style={{
62
+ left: `${question.midLabelIndex !== undefined && question.options.length > 1
63
+ ? (question.midLabelIndex / (question.options.length - 1)) * 100
64
+ : 50
65
+ }%`,
66
+ }}
67
+ dangerouslySetInnerHTML={{ __html: question.midLabel }}
68
+ />
69
+ )}
70
+ <span
71
+ className="absolute right-0 top-0 max-w-[30%] text-right leading-tight"
72
+ dangerouslySetInnerHTML={{ __html: question.maxLabel ?? '' }}
73
+ />
74
+ </div>
75
+ )}
76
+ <RatingScale
77
+ questionId={question.id}
78
+ options={question.options}
79
+ selectedValue={selectedValue}
80
+ onSelect={onSelect}
81
+ />
82
+ </div>
83
+ )}
84
+
85
+ {question.type === 'radio' && (
86
+ <div className="space-y-3">
87
+ {question.options.map(option => {
88
+ const isSelected = selectedValue === option.value;
89
+ return (
90
+ <label
91
+ key={String(option.value)}
92
+ className={`flex items-center gap-4 rounded-lg border py-4 px-5 cursor-pointer transition-all ${isSelected ? 'border-[#e20074] bg-[#fdf2f8]' : 'border-[#e5e5e5] bg-white hover:bg-gray-50/50'
93
+ }`}
94
+ >
95
+ <input
96
+ type="radio"
97
+ name={question.id}
98
+ value={option.value === null ? '' : option.value}
99
+ checked={isSelected}
100
+ onChange={() => onSelect(option.value)}
101
+ className="sr-only"
102
+ />
103
+ <div
104
+ className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition-all ${isSelected ? 'border-2 border-[#e20074] bg-white' : 'border-gray-400 bg-white'
105
+ }`}
106
+ >
107
+ {isSelected && <div className="h-2.5 w-2.5 rounded-full bg-[#e20074]" />}
108
+ </div>
109
+ <span
110
+ className="text-[15px] font-medium text-gray-900 leading-tight"
111
+ dangerouslySetInnerHTML={{ __html: option.label }}
112
+ />
113
+ </label>
114
+ );
115
+ })}
116
+ </div>
117
+ )}
118
+
119
+ {question.type === 'text' && (
120
+ <div className="space-y-1.5">
121
+ <textarea
122
+ rows={4}
123
+ value={(selectedValue as string | number | null) ?? ''}
124
+ onChange={e => onSelect(e.target.value)}
125
+ placeholder={question.placeholder || 'Type your response here...'}
126
+ className="w-full rounded-[4px] border border-gray-300 p-4 text-sm outline-none transition-colors focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
127
+ />
128
+ <div className="text-right text-xs text-gray-500 pr-1">
129
+ {Math.max(
130
+ 0,
131
+ (question.maxCharacterCount ?? 1000) - (typeof selectedValue === 'string' ? selectedValue.length : 0)
132
+ )}{' '}
133
+ Zeichen verbleiben.
134
+ </div>
135
+ </div>
136
+ )}
137
+
138
+ {question.type === 'csat' && (
139
+ <div className="space-y-3">
140
+ {(question.minLabel || question.maxLabel) && (
141
+ <div className="flex w-full justify-between text-xs font-semibold text-gray-500">
142
+ {question.minLabel && <span dangerouslySetInnerHTML={{ __html: question.minLabel }} />}
143
+ {question.maxLabel && <span dangerouslySetInnerHTML={{ __html: question.maxLabel }} />}
144
+ </div>
145
+ )}
146
+ <CsatScale
147
+ question={question}
148
+ selectedValue={selectedValue as string | number | null}
149
+ onSelect={onSelect}
150
+ />
151
+ </div>
152
+ )}
153
+
154
+ {/* matrix: subType is REQUIRED — never route on type alone */}
155
+ {question.type === 'matrix' && (question.subType === 'CSAT_MATRIX' || question.subType === 'RATING_MATRIX') && (
156
+ <div className="space-y-3">
157
+ <CsatMatrixScale
158
+ question={question}
159
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
160
+ onSelect={onSelect}
161
+ />
162
+ </div>
163
+ )}
164
+
165
+ {question.type === 'matrix' && question.subType === 'CFM_MATRIX' && (
166
+ <div className="space-y-3">
167
+ <LikertMatrixScale
168
+ question={question}
169
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
170
+ onSelect={onSelect}
171
+ />
172
+ </div>
173
+ )}
174
+
175
+ {question.type === 'slider_matrix' && (
176
+ <div className="space-y-3">
177
+ <SliderMatrixScale
178
+ question={question}
179
+ selectedValue={typeof selectedValue === 'object' && selectedValue !== null && !Array.isArray(selectedValue) ? selectedValue : {}}
180
+ onSelect={onSelect}
181
+ />
182
+ </div>
183
+ )}
184
+
185
+ {question.type === 'file_upload' && (
186
+ <div className="space-y-3">
187
+ <FileUploadScale
188
+ question={question}
189
+ selectedValue={selectedValue}
190
+ onSelect={onSelect}
191
+ />
192
+ </div>
193
+ )}
194
+
195
+ {question.type === 'text_and_media' && question.mediaUrl && (
196
+ <div className={`mt-4 flex w-full ${
197
+ question.mediaAlignment === 'TOP_CENTER' || question.mediaAlignment === 'BOTTOM_CENTER'
198
+ ? 'justify-center'
199
+ : question.mediaAlignment === 'TOP_RIGHT' || question.mediaAlignment === 'BOTTOM_RIGHT'
200
+ ? 'justify-end'
201
+ : 'justify-start'
202
+ }`}>
203
+ <div
204
+ style={{ width: question.mediaSize ? `${question.mediaSize}%` : '100%' }}
205
+ className="flex flex-col gap-2"
206
+ >
207
+ {question.mediaMimeType?.startsWith('video/') || question.mediaMimeType === 'VIDEO' ? (
208
+ <video controls src={question.mediaUrl} className="max-w-full rounded-md shadow-sm" />
209
+ ) : (
210
+ <img src={question.mediaUrl} alt={question.mediaTitle || ''} className="max-w-full rounded-md shadow-sm" />
211
+ )}
212
+ {question.mediaTitle && (
213
+ <p className="text-sm text-gray-500 text-center">{question.mediaTitle}</p>
214
+ )}
215
+ </div>
216
+ </div>
217
+ )}
218
+
219
+ {/* rating_scale / slider: implement per 03-ui-specs/05-rating-scale.md and 06-slider.md if present in survey */}
220
+ {question.type === 'rating_scale' && (
221
+ <div className="space-y-3">
222
+ <CsatScale
223
+ question={{ ...question, type: 'csat', buttonType: question.scaleStyle === 'star' ? 'star' : 'emoji' }}
224
+ selectedValue={selectedValue as number | null}
225
+ onSelect={onSelect}
226
+ />
227
+ </div>
228
+ )}
229
+
230
+ {question.type === 'slider' && (
231
+ <div className="space-y-3">
232
+ {/* Wire CustomSliderTrack — min/max/step from question; see 03-ui-specs/06-slider.md */}
233
+ </div>
234
+ )}
235
+
236
+ {validationError && (
237
+ <div className="mt-4 rounded-[4px] border border-[#333333] bg-[#d9d9d9] py-3.5 px-4 text-left">
238
+ <p className="text-[13px] sm:text-sm font-bold text-gray-900 leading-tight">{validationError}</p>
239
+ </div>
240
+ )}
241
+ </section>
242
+ );
243
+ }
@@ -42,6 +42,20 @@ Exhaustive switch — check each:
42
42
  - [ ] `dangerouslySetInnerHTML` for `question.text` / `description`
43
43
  - [ ] `validationError` banner per question
44
44
  - [ ] `id={question.id}` on every question wrapper
45
+ - [ ] Copied from `docs/templates/Question.tsx` — no stub/default fallback branches
46
+
47
+ ## 3b. Required Component Files (from fetched survey)
48
+
49
+ Fill from `00-integration/component-checklist.md` — every file below that applies to your survey **must exist** before Phase 5:
50
+
51
+ | Component file | Required if survey contains | Created? |
52
+ |----------------|----------------------------|----------|
53
+ | `LikertMatrixScale.tsx` | `matrix` + `CFM_MATRIX` | |
54
+ | `CsatMatrixScale.tsx` | `matrix` + `CSAT_MATRIX` or `RATING_MATRIX` | |
55
+ | `SliderMatrixScale.tsx` | `slider_matrix` | **blocking** |
56
+ | `FileUploadScale.tsx` | `file_upload` | **blocking** |
57
+ | `MatrixDropdown.tsx` | matrix with dropdown/selectbox modes | |
58
+ | `CustomSliderTrack.tsx` | `slider_matrix` or CSAT matrix `graphics` | |
45
59
 
46
60
  ## 4. Component Architecture Checklist
47
61
 
@@ -69,8 +83,9 @@ Fill one row per question in the fetched survey:
69
83
 
70
84
  - **configs detected:** non-default fields from `02-reference/config-field-index.md` (e.g. `matrixFormat: carousel`, `buttonType: emoji`)
71
85
  - **uiSpec read:** path from `MANIFEST.json` `uiSpecs` (matrix: use `matrix_{subType}`)
72
- - **ref component:** `apps/client/src/components/{Component}.tsx`
86
+ - **ref component:** monorepo: `apps/client/src/components/{Component}.tsx`; npm: implement from ui-spec only
73
87
  - **branches implemented:** list decision-tree branches from the ui-spec (e.g. dropdown vs grid vs carousel)
88
+ - **blocking types:** `matrix`, `slider_matrix`, `file_upload` must show interactive UI, not placeholder text
74
89
 
75
90
  ## 5. Theme, Brand & Logo
76
91
 
@@ -89,3 +104,15 @@ Fill one row per question in the fetched survey:
89
104
  - [ ] No imports from `.../src/` internal paths
90
105
  - [ ] All mutations via `onAction` — no direct state mutation
91
106
  - [ ] `npm run build` passes
107
+
108
+ ## 7b. Stub-Free Verification
109
+
110
+ Per question type in the fetched survey, confirm interactive UI renders (not grey placeholder text):
111
+
112
+ | type | subType | Renders real component? | No "not yet implemented" text? |
113
+ |------|---------|---------------------------|--------------------------------|
114
+ | | | | |
115
+
116
+ - [ ] `grep -r "not yet implemented" src/ components/ app/` returns **zero matches**
117
+ - [ ] Matrix questions route by `subType` in `Question.tsx`
118
+ - [ ] `slider_matrix` shows sliders; `file_upload` shows dropzone