@explorer02/cfm-survey-sdk 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/cli/index.js +140 -33
  2. package/dist/cli/index.mjs +140 -33
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/package.json +8 -1
  6. package/postinstall.js +8 -5
  7. package/templates/AGENT.md +62 -17
  8. package/templates/deploy-wizard-to-s3.sh +52 -0
  9. package/templates/docs/00-integration/agent-execution-flow.md +281 -0
  10. package/templates/docs/00-integration/agent-operating-contract.md +260 -0
  11. package/templates/docs/00-integration/analytics-events-catalog.md +54 -0
  12. package/templates/docs/00-integration/apply-ui-config.md +365 -0
  13. package/templates/docs/00-integration/canonical-survey-fixtures.md +94 -0
  14. package/templates/docs/00-integration/client-integration-guide.md +10 -8
  15. package/templates/docs/00-integration/client-lib-folder.md +115 -0
  16. package/templates/docs/00-integration/component-checklist.md +90 -54
  17. package/templates/docs/00-integration/constraints.md +13 -5
  18. package/templates/docs/00-integration/custom-field-logic-and-navigation.md +56 -0
  19. package/templates/docs/00-integration/display-logic-and-navigation.md +16 -1
  20. package/templates/docs/00-integration/file-upload-aws.md +116 -0
  21. package/templates/docs/00-integration/logic-fields-catalog.md +105 -0
  22. package/templates/docs/00-integration/partial-save-and-recovery.md +24 -0
  23. package/templates/docs/00-integration/progress.md +4 -2
  24. package/templates/docs/00-integration/question-type-sdk-matrix.md +11 -9
  25. package/templates/docs/00-integration/setup.md +15 -3
  26. package/templates/docs/00-integration/skip-logic-and-navigation.md +1 -1
  27. package/templates/docs/00-integration/survey-lifecycle-analytics.md +1 -1
  28. package/templates/docs/00-integration/ui-customization-wizard.md +144 -0
  29. package/templates/docs/00-integration/useSurveySDK.md +35 -1
  30. package/templates/docs/00-integration/wizard-api.md +134 -0
  31. package/templates/docs/00-integration/wizard-config-handoff.md +246 -0
  32. package/templates/docs/00-integration/wizard-preview-build-guide.md +400 -0
  33. package/templates/docs/00-integration/wizard-question-type-styling.md +195 -0
  34. package/templates/docs/01-components/01-survey-page.md +18 -8
  35. package/templates/docs/01-components/02-question.md +2 -2
  36. package/templates/docs/01-components/03-rating-scale.md +12 -18
  37. package/templates/docs/01-components/05-csat-matrix-scale.md +18 -26
  38. package/templates/docs/01-components/06-likert-matrix-scale.md +19 -38
  39. package/templates/docs/01-components/07-slider-matrix-scale.md +48 -33
  40. package/templates/docs/01-components/08-file-upload-scale.md +15 -52
  41. package/templates/docs/01-components/09-custom-slider-track.md +15 -25
  42. package/templates/docs/01-components/10-header-footer.md +23 -4
  43. package/templates/docs/01-components/11-progress-bar.md +14 -6
  44. package/templates/docs/01-components/13-matrix-dropdown.md +28 -33
  45. package/templates/docs/01-components/17-heatmap-scale.md +4 -4
  46. package/templates/docs/01-components/18-rank-order-scale.md +24 -31
  47. package/templates/docs/01-components/19-survey-sticky-chrome.md +79 -0
  48. package/templates/docs/01-components/README.md +2 -2
  49. package/templates/docs/02-reference/question-types/10-slider-matrix.md +38 -26
  50. package/templates/docs/02-reference/question-types/11-file-upload.md +36 -21
  51. package/templates/docs/02-reference/question-types/README.md +11 -1
  52. package/templates/docs/02-reference/routing-table.md +1 -0
  53. package/templates/docs/02-reference/value-derivation.md +1 -1
  54. package/templates/docs/03-ui-specs/00-question-shell.md +10 -0
  55. package/templates/docs/03-ui-specs/01-rating.md +28 -2
  56. package/templates/docs/03-ui-specs/02-radio.md +94 -24
  57. package/templates/docs/03-ui-specs/03-text.md +12 -1
  58. package/templates/docs/03-ui-specs/04-csat.md +90 -26
  59. package/templates/docs/03-ui-specs/06-slider.md +3 -1
  60. package/templates/docs/03-ui-specs/07-matrix-cfm.md +74 -35
  61. package/templates/docs/03-ui-specs/08-matrix-csat-rating.md +50 -16
  62. package/templates/docs/03-ui-specs/09-slider-matrix.md +70 -26
  63. package/templates/docs/03-ui-specs/10-file-upload.md +52 -20
  64. package/templates/docs/03-ui-specs/11-text-and-media.md +4 -0
  65. package/templates/docs/03-ui-specs/12-survey-chrome.md +92 -5
  66. package/templates/docs/03-ui-specs/13-heatmap.md +21 -3
  67. package/templates/docs/03-ui-specs/14-rank-order.md +108 -28
  68. package/templates/docs/03-ui-specs/README.md +11 -7
  69. package/templates/docs/03-ui-specs/shared/custom-slider-track.md +28 -35
  70. package/templates/docs/03-ui-specs/shared/matrix-dropdown.md +1 -1
  71. package/templates/docs/MANIFEST.json +187 -6
  72. package/templates/docs/index.md +116 -116
  73. package/templates/docs/templates/CsatMatrixScale.tsx +637 -0
  74. package/templates/docs/templates/CustomSliderTrack.tsx +144 -0
  75. package/templates/docs/templates/FileUploadScale.tsx +262 -0
  76. package/templates/docs/templates/HeatmapScale.tsx +114 -0
  77. package/templates/docs/templates/LikertMatrixScale.tsx +414 -0
  78. package/templates/docs/templates/MatrixDropdown.tsx +216 -0
  79. package/templates/docs/templates/Question.tsx +3 -0
  80. package/templates/docs/templates/RankOrderScale.tsx +353 -0
  81. package/templates/docs/templates/RatingScale.tsx +129 -0
  82. package/templates/docs/templates/SliderMatrixScale.tsx +248 -0
  83. package/templates/docs/templates/SurveyStickyChrome.tsx +24 -0
  84. package/templates/docs/templates/customFieldValues.ts +36 -0
  85. package/templates/docs/templates/implementation_plan.md +75 -8
  86. package/templates/docs/templates/labelStyles.ts +33 -0
  87. package/templates/docs/templates/selectionStyles.ts +100 -0
  88. package/templates/docs/templates/survey-inventory.schema.json +28 -23
  89. package/templates/docs/templates/surveyUiIcons.tsx +63 -0
  90. package/templates/docs/templates/surveyUiScaleUtils.ts +51 -0
  91. package/templates/docs/templates/verify-agent-build.sh +151 -0
  92. package/templates/preview-harness/preview-bridge.inline.js +201 -0
  93. package/templates/preview-harness/previewPages.js +108 -0
  94. package/templates/preview-harness/previewPages.ts +298 -0
  95. package/templates/preview-harness/vite-app/src/PreviewConfigContext.tsx +67 -0
  96. package/templates/preview-harness/vite-app/src/QuestionPreview.tsx +122 -0
  97. package/templates/preview-harness/vite-app/src/SurveyPagePreview.tsx +75 -0
  98. package/templates/preview-harness/vite-app/src/fixtures/questions.ts +311 -0
  99. package/templates/preview-harness/vite-app/src/globals.css +16 -0
  100. package/templates/preview-harness/vite-app/src/mount.tsx +9 -0
  101. package/templates/preview-harness/vite-app/src/preview-live-overrides.css +101 -0
  102. package/templates/preview-harness/vite-app/stubs/next-dynamic.tsx +25 -0
  103. package/templates/preview-harness/vite-app/stubs/next-image.tsx +29 -0
  104. package/templates/previewBridge.ts +153 -0
  105. package/templates/survey-theme.css +295 -0
  106. package/templates/survey-ui-config.schema.json +213 -0
  107. package/templates/wizard-dist/assets/PreviewMock-DgHfrVeb.js +1 -0
  108. package/templates/wizard-dist/assets/TypePanel-CFVC3Ptn.js +1 -0
  109. package/templates/wizard-dist/assets/index-BhWM50Yu.css +1 -0
  110. package/templates/wizard-dist/assets/index-DYK3X1e5.js +34 -0
  111. package/templates/wizard-dist/assets/vendor-BwkXDkd3.js +17 -0
  112. package/templates/wizard-dist/index.html +20 -0
  113. package/templates/wizard.html +13 -1129
@@ -1,40 +1,34 @@
1
1
  # Rating Scale Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/01-rating.md` (NPS) · `03-ui-specs/05-rating-scale.md` (star/emoji gap)
4
- > **Reference implementation:** `apps/client/src/components/RatingScale.tsx`
4
+ > **Portable template:** copy [`templates/RatingScale.tsx`](../templates/RatingScale.tsx) + [`surveyUiScaleUtils.ts`](../templates/surveyUiScaleUtils.ts) + [`surveyUiIcons.tsx`](../templates/surveyUiIcons.tsx)
5
5
  > **Target Component**: `RatingScale`
6
6
  > **Handles**: `type: 'NPS_SCALE'` (via `QUESTION_TYPE.NPS_SCALE`)
7
7
 
8
8
  ## Core Responsibility
9
- Render a horizontal sequence of numbered badges.
9
+
10
+ Render a horizontal sequence of numbered badges with traffic-light colors (NPS 0–10).
10
11
 
11
12
  ## Configuration Matrix
12
- You MUST handle the following configurations dynamically:
13
13
 
14
14
  ### 1. Color Grading (NPS)
15
- - Check if `option.color` exists. If so, apply it to the badge background. This is crucial for NPS scales (Red 0-6, Yellow 7-8, Green 9-10).
15
+ - Use `getAccentColor(value)` from `surveyUiScaleUtils.ts` for badge backgrounds
16
16
 
17
17
  ### 2. Label Positioning (`minLabel`, `midLabel`, `maxLabel`)
18
- - **Architectural Requirement**: Wrap the badge sequence and the labels in a relative container.
19
- - Place labels underneath the badges using absolute positioning.
20
- - **Mid Label Calculation**: You must calculate the exact center position: `style={{ left: \`\${(midLabelIndex / (options.length - 1)) * 100}%\` }}`.
18
+ - Render in `Question.tsx` above `RatingScale` with absolute positioning
19
+ - Mid label: `left: (midLabelIndex / (options.length - 1)) * 100%`
21
20
 
22
21
  ## UI/UX Rules (CRITICAL)
23
22
 
24
- The UI must exactly match enterprise standards:
25
-
26
- 1. **Shape**: Badges MUST be square with rounded corners (`rounded-lg w-10 h-10 sm:w-12 sm:h-12`). Do NOT use full circles (`rounded-full`).
27
- 2. **Unselected State**: If any item is selected, unselected items must dim (`opacity-30`).
28
- 3. **Selected State**: The selected badge MUST pop out prominently: `border-2 border-[#e20074] shadow-md scale-105 opacity-100 font-bold`.
29
- 4. **Hover State**: `hover:scale-105 hover:border-[#e20074] transition-all cursor-pointer`.
30
- 5. **Tooltips**: Every badge MUST have `title={option.label}` applied for accessibility and semantic context (e.g. hovering '10' shows 'Extremely Likely').
23
+ 1. **Shape**: Square badges with rounded corners — not full circles
24
+ 2. **Unselected State**: Dim unselected when any item selected (`opacity-30`)
25
+ 3. **Selected State**: `border-2 border-[#e20074] shadow-md scale-105`
26
+ 4. **Tooltips**: `title={option.label}` on every badge
31
27
 
32
28
  ## SDK Integration Checklist (agent)
33
29
 
30
+ - [ ] Copied portable `RatingScale.tsx`
34
31
  - [ ] Type literal: `QUESTION_TYPE.NPS_SCALE`
35
32
  - [ ] Answer stored as: `number`
36
33
  - [ ] onAction CHANGE: `{ questionId, answerValue: number }`
37
- - [ ] Visibility helper: `getVisibleScaleColumnsForAnswers` when scale points have answer logic
38
- - [ ] Custom fields: 4th arg on scale visibility helper when needed
39
- - [ ] Logic features: skip ✓ display ✓ answer ✓ variants ✓ numbering ✓
40
- - [ ] Matrix row: [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md#nps_scale)
34
+ - [ ] minLabel/maxLabel rendered in Question.tsx when present
@@ -1,44 +1,36 @@
1
1
  # CSAT Matrix Architectural Blueprint
2
2
 
3
- > **UI spec (authoritative):** `03-ui-specs/04-csat.md` (for `CSAT_MATRIX`) and `03-ui-specs/08-matrix-csat-rating.md` (for `RATING_MATRIX`)
4
- > **Reference implementation:** `apps/client/src/components/CsatMatrixScale.tsx`
3
+ > **UI spec (authoritative):** `03-ui-specs/04-csat.md` (CSAT) · `03-ui-specs/08-matrix-csat-rating.md` (RATING)
4
+ > **Portable template:** copy [`templates/CsatMatrixScale.tsx`](../templates/CsatMatrixScale.tsx) + shared deps (see ui-spec)
5
5
  > **Target Component**: `CsatMatrixScale`
6
6
  > **Handles**: `type: 'CSAT_MATRIX'` and `type: 'RATING_MATRIX'`
7
7
 
8
8
  ## Core Responsibility
9
9
 
10
- Render a grid where columns represent satisfaction scales (Emojis, Stars, or Rating Badges).
10
+ Render a matrix grid where rows are `statementRows` and columns are `scaleColumns`. Route on `displayStyle` and `gridLayout`.
11
11
 
12
- ## Data Mapping & State Management
12
+ ## Statement & labels (CRITICAL)
13
13
 
14
- - `selectedValue`: `MatrixRowAnswers` keys are `statementRows[].id`, values are column **`id`** (via `matrixColumnStoredValue`)
15
- - Iterate over `question.statementRows`. Within each row, iterate over `question.scaleColumns`.
16
- - Render the correct icon/badge in the `<td>` based on `question.displayStyle`.
14
+ Every grid row MUST show `statementRows[].statementText` in a **180px** left column.
17
15
 
18
- ## Configuration Matrix
19
-
20
- You MUST handle:
21
-
22
- - **`hasNotApplicableOption`**: If true, the last column might be `null`. Render a distinct UI for the N/A column.
23
- - **`reverseScaleOrder`**: Reverse column order before render when true.
24
- - **`gridLayout`**: `'standard'`, `'carousel'`, or `'dropdown'` — see ui-spec decision tree.
16
+ Every grid MUST show either:
17
+ - A **column header row** from `scaleColumns[].label`, OR
18
+ - An **anchor label row** from `scaleAnchorLabels[]`
25
19
 
26
- ## UI/UX Bounding Box Rules (CRITICAL)
20
+ Never render a bare grid of inputs with no row/column labels.
27
21
 
28
- Matrices are difficult to read and click accurately. You MUST implement these strict UI rules:
29
-
30
- 1. **Row Tracking**:
31
- - Every data `<tr>` MUST have `hover:bg-gray-50 transition-colors duration-150`.
22
+ ## Configuration Matrix
32
23
 
33
- 2. **The "Active Bounding Box"**:
34
- - For Emojis and Stars inside the matrix, do NOT just change the SVG fill.
35
- - The interactive wrapper inside the `<td>` MUST toggle a pink bounding box.
24
+ - **`displayStyle`**: emoji | star | numbered | graphics | standard — each has distinct UI (see ui-spec)
25
+ - **`hasNotApplicableOption`**: 56px N/A column
26
+ - **`reverseScaleOrder`**: flip column index before icon/badge lookup
27
+ - **`gridLayout`**: standard | carousel | dropdown
36
28
 
37
29
  ## SDK Integration Checklist (agent)
38
30
 
31
+ - [ ] Copied portable template + `surveyUiIcons.tsx` + `surveyUiScaleUtils.ts`
39
32
  - [ ] Type literals: `QUESTION_TYPE.CSAT_MATRIX` and `QUESTION_TYPE.RATING_MATRIX`
40
- - [ ] Answer stored as: `MatrixRowAnswers` via `matrixColumnStoredValue`
41
- - [ ] onAction CHANGE: `{ questionId, answerValue: MatrixRowAnswers }`
42
- - [ ] Visibility helper: `getVisibleMatrixItemsForAnswers(..., customFieldValues?)`
43
- - [ ] Emoji/star UI: client-side `react-icons` — not SDK exports
33
+ - [ ] Answer: `MatrixRowAnswers` via `matrixColumnStoredValue(col)`
34
+ - [ ] Visibility: `getVisibleMatrixItemsForAnswers(..., customFieldValues?)`
35
+ - [ ] Row labels + column/anchor labels visible
44
36
  - [ ] Matrix row: [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md)
@@ -1,55 +1,36 @@
1
1
  # Likert Matrix Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/07-matrix-cfm.md`
4
- > **Reference implementation:** `apps/client/src/components/LikertMatrixScale.tsx`
4
+ > **Portable template:** copy [`templates/LikertMatrixScale.tsx`](../templates/LikertMatrixScale.tsx) + [`MatrixDropdown.tsx`](../templates/MatrixDropdown.tsx)
5
5
  > **Target Component**: `LikertMatrixScale`
6
6
  > **Handles**: `type: 'CFM_MATRIX'`
7
7
 
8
8
  ## Core Responsibility
9
- Render a grid with text-label columns. However, this component must be highly dynamic to support complex layouts, transpositions, and long-form matrix readability.
10
9
 
11
- ## Data Mapping & State Management
12
- - `selectedValue`: `MatrixRowAnswers` — store column **`id`** via `matrixColumnStoredValue(col)`
13
- - **Mutation**: When a cell is selected for `row.id`, dispatch `onSelect({ ...selectedValue, [row.id]: columnId })`
10
+ Render CFM matrix grids with support for bipolar layout, transposed tables, multi-select checkboxes, and anchor labels.
14
11
 
15
- ## Configuration Matrix (CRITICAL FOR PLANNING)
12
+ ## Statement & labels (CRITICAL)
16
13
 
17
- Your component architecture MUST account for the following boolean flags from `question`:
14
+ | Mode | Left label | Right label | Column headers |
15
+ |------|------------|-------------|----------------|
16
+ | Likert | `statementText` (180px) | — | `scaleColumns[].label` when `showColumnHeaders` |
17
+ | Bipolar | `statementText` (25%) | `oppositeStatementText` (25%) | anchor labels OR column headers |
18
+ | Transposed | `scaleColumns[].label` as row | — | `statementRows[].statementText` as columns |
18
19
 
19
- ### 1. `transposeTable`
20
- If `true`, the standard Row-by-Column matrix is inverted:
21
- - `question.columns` become the row labels on the left.
22
- - `question.rows` become the top column headers.
23
- - **Architectural Requirement**: Your `<tbody>` map must iterate over `question.columns` first, and render `<td>` cells mapped from `question.rows`.
24
- - Ensure `onSelect` still correctly pairs the original `row.id` with the original `col.value`.
20
+ Use `dangerouslySetInnerHTML` for all statement/label HTML.
25
21
 
26
- ### 2. `repeatScale`
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
- - **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>`.
22
+ ## Configuration Matrix
29
23
 
30
- ### 3. `matrixType === 'bipolar'`
31
- Standard matrices (`'likert'`) only show statement text on the left. Bipolar scales show text on BOTH sides of the radio buttons.
32
- - **Implementation Strategy**: Render an extra `<td>` at the end of the row containing `row.rightText`.
33
-
34
- ## UI/UX & Styling Constraints
35
-
36
- To match enterprise standards, implement the following UI strictly:
37
-
38
- 1. **Row Tracking (Hover Effects)**:
39
- - Every data `<tr>` MUST have `hover:bg-gray-50 transition-colors duration-150 group`. This is mandatory for UX on wide matrices.
40
- 2. **Radio Buttons**:
41
- - Use `accent-[#e20074] w-5 h-5 cursor-pointer` or build custom CSS circles that fill with the brand color.
42
- - Do NOT use standard unstyled blue browser radios.
43
- 3. **Tooltips (Accessibility)**:
44
- - Every input MUST have `title={col.label}` so the user sees the semantic meaning when hovering the radio button.
45
- 4. **Header Typography**:
46
- - `th` tags should be `text-xs font-semibold text-gray-500 uppercase tracking-wider p-4 border-b`.
24
+ - **`transposeTable`**: swaps which array becomes grid rows vs columns
25
+ - **`selectionMode: 'multiple'`**: checkboxes; answers are `string[]` per row
26
+ - **`statementLayout: 'bipolar'`**: 25% | scale | 25% layout
27
+ - **`scaleAnchorLabels`**: positioned above scale when bipolar or likert
47
28
 
48
29
  ## SDK Integration Checklist (agent)
49
30
 
31
+ - [ ] Copied portable template
50
32
  - [ ] Type literal: `QUESTION_TYPE.CFM_MATRIX`
51
- - [ ] Answer stored as: `MatrixRowAnswers` via `matrixColumnStoredValue`
52
- - [ ] onAction CHANGE: `{ questionId, answerValue: MatrixRowAnswers }`
53
- - [ ] Visibility helper: `getVisibleMatrixItemsForAnswers(..., customFieldValues?)`
54
- - [ ] Handle `gridLayout`: standard / carousel / dropdown
55
- - [ ] Matrix row: [`question-type-sdk-matrix.md#cfm_matrix`](../00-integration/question-type-sdk-matrix.md#cfm_matrix)
33
+ - [ ] Answer: `MatrixRowAnswers` via `matrixColumnStoredValue(col)`
34
+ - [ ] Visibility: `getVisibleMatrixItemsForAnswers(..., customFieldValues?)`
35
+ - [ ] All row labels and header labels visible per ui-spec
36
+ - [ ] Matrix row: [`question-type-sdk-matrix.md`](../00-integration/question-type-sdk-matrix.md)
@@ -1,56 +1,71 @@
1
1
  # Slider Matrix Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/09-slider-matrix.md`
4
- > **Reference implementation:** `apps/client/src/components/SliderMatrixScale.tsx`
4
+ > **Portable template:** copy [`templates/SliderMatrixScale.tsx`](../templates/SliderMatrixScale.tsx) + [`CustomSliderTrack.tsx`](../templates/CustomSliderTrack.tsx) + [`surveyUiScaleUtils.ts`](../templates/surveyUiScaleUtils.ts)
5
5
  > **Target Component**: `SliderMatrixScale`
6
6
  > **Handles**: `type: 'SLIDER_MATRIX'`
7
7
 
8
8
  ## Core Responsibility
9
- Render an independent range slider for every row in the matrix. Sliders must be highly visible, utilizing a "thick track" UI, dynamic fill based on the current value, and exact value badges.
10
9
 
11
- ## State Management
12
- - `selectedValue`: Expects `MatrixRowAnswers` shape.
13
- - `row.min`, `row.max`, and `row.step` must dictate the boundaries of the slider logic.
14
- - **Mutation**: `onChange` of the input must parse the value as a Number before dispatching `onSelect`.
10
+ Render one `CustomSliderTrack` per `statementRows[]` entry, with a shared header row of colored tick badges and optional anchor labels.
15
11
 
16
- ## The "Thick Slider" UX Implementation (CRITICAL)
12
+ ## CRITICAL Do NOT improvise
17
13
 
18
- Native `<input type="range">` elements are notoriously difficult to style consistently across browsers. You MUST implement the "Invisible Overlay" pattern to achieve the expected thick-track UI.
14
+ - **Do NOT** build custom “thick slider” / invisible overlay range inputs
15
+ - **Do NOT** use legacy field names (`rows`, `labels`, `tickValues`, `enableNotApplicable`, `sliderType`)
16
+ - **Copy the portable template first**, then customize branding colors only
19
17
 
20
- ### The Invisible Overlay Pattern
21
- For every slider row, build a relative container:
22
- 1. **Base Track**: `absolute w-full h-2 bg-gray-200 rounded-full`.
23
- 2. **Dynamic Fill Track**: `absolute h-2 bg-[#e20074] rounded-l-full`.
24
- - Calculate width dynamically: `((value - min) / (max - min)) * 100 + '%'`.
25
- 3. **The Thumb**: `absolute w-5 h-5 bg-[#e20074] border-2 border-white rounded-full shadow-md pointer-events-none transition-transform group-hover:scale-125`.
26
- - Position it dynamically: `left: calc(${percentage}% - 10px)`.
27
- 4. **The Input Control**: Layer a native input on top, completely invisible but clickable:
28
- - `<input type="range" className="absolute w-full h-full opacity-0 cursor-pointer z-10" />`
18
+ ## SDK Fields (authoritative)
29
19
 
30
- ## Configuration Matrix (CRITICAL FOR PLANNING)
20
+ From `fetchSurvey/types/sliderMatrix.ts`:
31
21
 
32
- Your component must account for:
22
+ | Field | UI role |
23
+ |-------|---------|
24
+ | `statementRows[]` | Row labels + per-row min/max/step |
25
+ | `ticks.count` + `ticks.marks[]` | Header colored badges (`tickPosition`, `tickLabel`) |
26
+ | `scaleAnchorLabels?` | Optional text labels above tick row |
27
+ | `enableInputBox` | 80px numeric input + steppers |
28
+ | `displayValues` | Pass to `CustomSliderTrack` |
29
+ | `hasNotApplicableOption` | N/A checkbox column; answer value **`'N/A'`** string |
30
+ | `sliderStyle?: 'graphics'` | Plain text ticks + emoji thumb via `CustomSliderTrack` |
33
31
 
34
- ### 1. Exact Value Display
35
- - Users must see the exact number they are selecting.
36
- - Render a badge at the far right of the slider track: `shrink-0 w-8 text-center font-bold text-[#e20074]`.
32
+ ## Layout (matches reference)
37
33
 
38
- ### 2. `row.ticks` & `tickValues`
39
- - If `question.ticks` or `row.tickValues` exists, you must render tiny vertical tick marks below the track at appropriate percentage intervals to guide the user.
34
+ ```
35
+ [25% spacer] [scaleAnchorLabels?] [colored tick badges 0–10] [input spacer?] [N/A header?]
36
+ ┌─ gray row band #f3f4f6 ─────────────────────────────────────────────────────────────┐
37
+ │ 25% statementText │ CustomSliderTrack │ 80px input? │ N/A checkbox? │
38
+ └─────────────────────────────────────────────────────────────────────────────────────┘
39
+ ```
40
40
 
41
- ### 3. Label Positioning
42
- - `row.minLabel` and `row.maxLabel` must be displayed directly underneath the far-left and far-right of the slider track using flexbox `justify-between text-xs text-gray-500`.
41
+ ## Statement text visibility
43
42
 
44
- ### 4. `enableNotApplicable`
45
- - If `true`, render a checkbox at the end of the row.
46
- - Checking it sets the value in the map to `null`.
47
- - When `value === null`, gray out the slider track and disable the input.
43
+ - Render `row.statementText` with `dangerouslySetInnerHTML` in the **25% left column**
44
+ - Skip rendering when text is empty or literal `"Question"` (placeholder from API)
45
+ - **Never** omit the statement column — agents that put statement text in the question title only will fail multi-row surveys
46
+
47
+ ## Header tick badges
48
+
49
+ ```typescript
50
+ const tickValues = ticks.marks.map((mark, index) => ({
51
+ value: mark.tickPosition,
52
+ label: mark.tickLabel,
53
+ color: tickColorFromIndex(index, ticks.count),
54
+ }));
55
+ const htmlStep = ticks.count > 1 ? (max - min) / (ticks.count - 1) : row.step;
56
+ ```
57
+
58
+ ## N/A behavior
59
+
60
+ - Checked → store `'N/A'` string (not `null`)
61
+ - Unchecked → reset to `row.defaultValue ?? row.min`
48
62
 
49
63
  ## SDK Integration Checklist (agent)
50
64
 
65
+ - [ ] Copied `SliderMatrixScale.tsx`, `CustomSliderTrack.tsx`, `surveyUiScaleUtils.ts`
51
66
  - [ ] Type literal: `QUESTION_TYPE.SLIDER_MATRIX`
52
- - [ ] Answer stored as: `MatrixRowAnswers` — number or `null` per row
67
+ - [ ] Answer: `MatrixRowAnswers` — `{ [rowId]: number | 'N/A' }`
53
68
  - [ ] onAction CHANGE: `{ questionId, answerValue: MatrixRowAnswers }`
54
- - [ ] Visibility helper: `getVisibleStatementRowsForAnswers(..., customFieldValues?)`
55
- - [ ] Use `CustomSliderTrack` when `sliderType === 'graphics'`
69
+ - [ ] Visibility: `getVisibleStatementRowsForAnswers(..., customFieldValues?)`
70
+ - [ ] Header row renders even for single-row questions
56
71
  - [ ] Matrix row: [`question-type-sdk-matrix.md#slider_matrix`](../00-integration/question-type-sdk-matrix.md#slider_matrix)
@@ -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)
@@ -1,38 +1,28 @@
1
1
  # Custom Slider Track Architectural Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/shared/custom-slider-track.md`
4
- > **Reference implementation:** `apps/client/src/components/CustomSliderTrack.tsx`
4
+ > **Portable template:** copy [`templates/CustomSliderTrack.tsx`](../templates/CustomSliderTrack.tsx) + [`surveyUiIcons.tsx`](../templates/surveyUiIcons.tsx)
5
5
  > **Target Component**: `CustomSliderTrack`
6
- > **Used By**: `SliderMatrixScale`, `CsatMatrixScale`, standalone `slider`
6
+ > **Used By**: `SliderMatrixScale`, `CsatMatrixScale` (`displayStyle: graphics`)
7
7
 
8
8
  ## Core Responsibility
9
- Native browser `<input type="range">` elements are impossible to style consistently across browsers, especially when trying to render dynamic emoji handles. This component MUST implement an invisible overlay architecture to provide total CSS control over the slider's appearance.
10
9
 
11
- ## The Invisible Overlay Architecture (CRITICAL)
10
+ Provide a styled range slider with optional emoji thumb (`sliderType: 'graphics'`). Used **inside** `SliderMatrixScale` and matrix graphics rows — not as a standalone question type.
12
11
 
13
- Instead of trying to style the `::-webkit-slider-thumb` pseudo-elements, you MUST build the slider using layered DOM elements.
12
+ ## CRITICAL Copy template
14
13
 
15
- ### Layer 1: The Background Track
16
- A static `<div>` representing the entire length of the slider.
17
- - CSS: `absolute w-full h-2 bg-gray-200 rounded-full`.
14
+ Do not reimplement from this blueprint alone. Copy `templates/CustomSliderTrack.tsx` which implements the layered track + invisible range input pattern.
18
15
 
19
- ### Layer 2: The Active Fill
20
- A dynamic `<div>` representing the portion of the slider up to the current value.
21
- - CSS: `absolute h-2 bg-[#e20074] rounded-l-full`.
22
- - Inline Style: `style={{ width: \`\${percentage}%\` }}`.
16
+ ## Pass tickLabels for matrix graphics
23
17
 
24
- ### Layer 3: The Custom Thumb
25
- A visual element that floats along the track.
26
- - **For Standard Sliders**: A pink circle. `w-5 h-5 bg-[#e20074] border-2 border-white rounded-full shadow-md pointer-events-none`.
27
- - **For Graphics Sliders (`sliderType === 'graphics'`)**: A larger white circle containing an emoji mapped to the current value using client-side react-icons. `w-10 h-10 bg-white shadow-md border flex items-center justify-center text-2xl`.
28
- - Inline Style: `style={{ left: \`\${percentage}%\` }}`.
18
+ ```typescript
19
+ tickLabels={scaleColumns.map(col => col.label)}
20
+ ```
29
21
 
30
- ### Layer 4: The Native Input (The Engine)
31
- An invisible native input overlaid exactly on top to capture all mouse/touch events automatically without needing manual math for drag interactions.
32
- - CSS: `<input type="range" className="absolute w-full h-full opacity-0 cursor-pointer z-10" />`.
33
- - This ensures perfect accessibility, keyboard navigation, and mobile touch support.
22
+ Required for correct tooltip text on graphics sliders (RATING_MATRIX Page 11 fixture).
34
23
 
35
- ## Configuration Logic
36
- - Calculate percentage carefully: `((value - min) / (max - min)) * 100`.
37
- - Ensure the custom thumb is negatively margined (e.g. `-ml-2.5` or `-ml-5`) so that its *center* aligns with the percentage point, not its left edge.
38
- - If `question.ticks` is provided, map out small `w-0.5 h-3 bg-gray-300` vertical lines below the track at the correct percentage intervals.
24
+ ## SDK Integration Checklist
25
+
26
+ - [ ] Copied portable `CustomSliderTrack.tsx`
27
+ - [ ] Used by `SliderMatrixScale` or `CsatMatrixScale` not wired directly in `Question.tsx` for SLIDER_MATRIX
28
+ - [ ] `hasSelectedValue`, `disabled`, `htmlStep` props wired correctly
@@ -1,17 +1,36 @@
1
1
  # Theme & Logo Engineering Blueprint
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/12-survey-chrome.md`
4
- > **Reference implementation:** `apps/client/src/components/Header.tsx`, `Footer.tsx`
4
+ > **Wizard contract:** [`wizard-preview-build-guide.md`](../00-integration/wizard-preview-build-guide.md) `data-cfm-*` + `var(--cfm-*)`
5
+ > **Monorepo snapshot (do not copy):** `apps/client/src/components/Header.tsx`, `Footer.tsx`
5
6
  > **Target Components**: `Header`, `Footer`
6
7
  > **Handles**: Branding, Mockup Replication
7
8
 
8
9
  ## Core Responsibility
9
10
  When building the survey layout, the client will provide a text prompt or an image mockup. You MUST dynamically build the Theme and Logo using purely CSS and Tailwind to match the requested brand.
10
11
 
12
+ ## Sticky content chrome
13
+
14
+ On the **content** phase, render `Header` inside `SurveyStickyChrome` with `embedded` so the wrapper owns the shadow:
15
+
16
+ ```tsx
17
+ <Header embedded />
18
+ ```
19
+
20
+ Use standalone `Header` (default, with shadow) on intro, end, paused, loading, and terminal pages. See `19-survey-sticky-chrome.md`.
21
+
11
22
  ## Logo Engineering Rules (CRITICAL)
12
- - **NO EXTERNAL IMAGES**: You are strictly forbidden from writing `<img src="url" />` for logos, because you do not have a CDN to host them.
13
- - **CSS-Only Logos**: You MUST construct the company logo using HTML and CSS primitives.
14
- - Example: If the prompt shows the Telekom logo (a white 'T' inside a pink square, or distinct abstract shapes), you must build it using Flexbox, CSS shapes (`rounded-full`, `rounded-tr-3xl`, absolute positioning), and the exact brand colors (`bg-[#e20074] text-white`).
23
+
24
+ Agents must implement **one** of these wizard-compatible patterns in `Header.tsx`:
25
+
26
+ | Mode | When | Required markup |
27
+ |------|------|-----------------|
28
+ | **Image** | Brand asset in `public/` | `<img data-cfm-logo src="/exact-file.svg" />` plus hidden `<span data-cfm-logo-text>` fallback |
29
+ | **Text** | CSS-only logo from prompt (no image file) | `<span data-cfm-logo-text>Company</span>` in logo slot — no `data-cfm-logo` img |
30
+
31
+ - **NO EXTERNAL URLS** for logos (`https://…`).
32
+ - **CSS-only logos** use `data-cfm-logo-text` (and optional CSS shapes) — wizard shows text until client uploads an image.
33
+ - After wizard upload, saved `global.logo.fileName` is used on subsequent sessions — see `wizard-preview-build-guide.md`.
15
34
 
16
35
  ## Theme Engineering
17
36
  - Map the primary brand color to the accent and border colors across the entire survey.
@@ -20,20 +20,28 @@ export default function ProgressBar({ progressPercentage }: ProgressBarProps) {
20
20
  const width = Math.min(100, Math.max(0, progressPercentage));
21
21
 
22
22
  return (
23
- <div className="mb-8 h-2 w-full overflow-hidden rounded-full bg-gray-100">
24
- <div
25
- className="h-full bg-[#e20074] transition-all duration-500 ease-out"
26
- style={{ width: `${width}%` }}
27
- />
23
+ <div className="w-full">
24
+ <div className="h-3 w-full rounded-full bg-gray-100">
25
+ <div
26
+ className="h-3 rounded-full bg-[#e20074] transition-all duration-300 ease-out"
27
+ style={{ width: `${width}%` }}
28
+ />
29
+ </div>
28
30
  </div>
29
31
  );
30
32
  }
31
33
  ```
32
34
 
35
+ Vertical spacing (`pt-4` above, `pb-5` below) lives on the **`SurveyStickyChrome`** wrapper — not on `ProgressBar` itself.
36
+
33
37
  ## Wiring
34
38
 
39
+ Place inside **`SurveyStickyChrome`** on the content phase (see `19-survey-sticky-chrome.md`):
40
+
35
41
  ```tsx
36
- <ProgressBar progressPercentage={state.progressPercentage} />
42
+ <SurveyStickyChrome progressPercentage={state.progressPercentage} />
37
43
  ```
38
44
 
45
+ Do not duplicate `ProgressBar` inside scrollable `<main>`.
46
+
39
47
  The SDK computes percentage from **questions on the active skip path**, not total survey pages. Skip logic and answer changes update the bar automatically.
@@ -1,46 +1,41 @@
1
1
  # Matrix Dropdown
2
2
 
3
- > **UI spec (authoritative):** `03-ui-specs/shared/matrix-dropdown.md`
4
- > **Reference implementation:** `apps/client/src/components/MatrixDropdown.tsx`
5
- > **Component**: `MatrixDropdown`
6
- > **Used by**: `CsatMatrixScale`, `LikertMatrixScale`
3
+ > **UI spec (authoritative):** [`03-ui-specs/shared/matrix-dropdown.md`](../03-ui-specs/shared/matrix-dropdown.md) — implement the **custom panel** (not a native `<select>`).
4
+ > **Portable template:** copy [`templates/MatrixDropdown.tsx`](../templates/MatrixDropdown.tsx) when available.
5
+ > **Component:** `MatrixDropdown`
6
+ > **Used by:** `CsatMatrixScale`, `LikertMatrixScale`
7
7
 
8
8
  ## Role
9
- Renders a dropdown selector for a single row in a matrix question. Used when `gridLayout === 'dropdown'` or `displayStyle === 'dropdown'`.
10
9
 
11
- Accepts `ScaleColumn[]`; emits stored column ids via `matrixColumnStoredValue(col)` see `02-reference/value-derivation.md`.
10
+ Renders a custom dropdown selector for a single matrix row. Used when `gridLayout === 'dropdown'` or `displayStyle` is `'dropdown'` / `'selectbox'`.
11
+
12
+ Emits stored column **`id`** values via `matrixColumnStoredValue(col)` — see [`02-reference/value-derivation.md`](../02-reference/value-derivation.md).
13
+
14
+ ## Props (wiring only)
12
15
 
13
- ## Props Interface
14
16
  ```typescript
15
17
  type MatrixDropdownProps = {
16
- columns: MatrixColumn[];
17
- selectedValue: string | number | null | undefined;
18
+ options: ScaleColumn[]; // visible columns for this row
19
+ value: string | number | null | undefined;
18
20
  onChange: (value: string | number | null) => void;
21
+ multiple?: boolean;
19
22
  placeholder?: string;
20
23
  };
21
24
  ```
22
25
 
23
- ## Structure
24
- Renders a native HTML `<select>` element.
25
-
26
- ```tsx
27
- <select
28
- value={selectedValue === null ? '' : String(selectedValue ?? '')}
29
- onChange={e => {
30
- const val = e.target.value;
31
- if (val === '') onChange(null); // Map empty string back to null for N/A
32
- else {
33
- // Find original option to keep type (number vs string) intact
34
- const opt = columns.find(c => String(c.value) === val);
35
- onChange(opt ? opt.value : val);
36
- }
37
- }}
38
- >
39
- <option value="" disabled>Select an option...</option>
40
- {columns.map(col => (
41
- <option key={String(col.value)} value={String(col.value)}>
42
- {col.label}
43
- </option>
44
- ))}
45
- </select>
46
- ```
26
+ ## Implementation
27
+
28
+ Do **not** use a native `<select>`. Follow the shared ui-spec for:
29
+
30
+ - Click-to-open panel with click-outside dismiss
31
+ - Single-select: one choice; multi-select: tag chips + toggle
32
+ - N/A mutual exclusion when `hasNotApplicableOption` applies
33
+ - Placeholder text: `"Select..."` (dropdown) or full-width cards (selectbox)
34
+
35
+ See [`03-ui-specs/shared/matrix-dropdown.md`](../03-ui-specs/shared/matrix-dropdown.md) for layout, states, and agent checklist.
36
+
37
+ ## SDK Integration Checklist
38
+
39
+ - [ ] Copy `templates/MatrixDropdown.tsx` or implement per shared ui-spec
40
+ - [ ] `onChange` stores column **`id`** via `matrixColumnStoredValue(col)`
41
+ - [ ] Filter columns with `getVisibleMatrixItemsForAnswers` before passing to dropdown
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **UI spec (authoritative):** `03-ui-specs/13-heatmap.md`
4
4
  > **Reference implementation:** `apps/client/src/components/HeatmapScale.tsx`
5
- > **Coordinate helpers:** `apps/client/src/lib/surveyUi/heatmapCoords.ts`
5
+ > **Portable coord helper:** copy [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts)
6
6
  > **Target Component**: `HeatmapScale`
7
7
  > **Handles**: `type: 'HEATMAP'`
8
8
 
@@ -15,8 +15,8 @@ Render the heatmap image, capture click spots in normalized 0–1 coordinates, t
15
15
  ```typescript
16
16
  type HeatmapScaleProps = {
17
17
  question: HeatmapQuestion;
18
- selectedValue?: HeatmapAnswer;
19
- onSelect: (answer: HeatmapAnswer | undefined) => void;
18
+ selectedValue?: HeatmapClickPoint[];
19
+ onSelect: (answer: HeatmapClickPoint[] | undefined) => void;
20
20
  };
21
21
  ```
22
22
 
@@ -34,7 +34,7 @@ type HeatmapScaleProps = {
34
34
 
35
35
  ## Coordinate Helpers
36
36
 
37
- Use `apps/client/src/lib/surveyUi/heatmapCoords.ts`:
37
+ Copy [`templates/heatmapCoords.ts`](../templates/heatmapCoords.ts):
38
38
 
39
39
  - `getNormalizedClickFromImage(event, imageElement)`
40
40
  - `findSpotNearClick(clickX, clickY, spots, imageRect)`