@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
@@ -0,0 +1,353 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Portable RankOrderScale template — copy to src/components/RankOrderScale.tsx
5
+ * Import SDK from your package.json: @explorer02/cfm-survey-sdk or @repo/sdk
6
+ * Requires: @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
7
+ * Next.js: add 'use client' (already present); optional dynamic(..., { ssr: false }) wrapper in Question.tsx
8
+ */
9
+
10
+ import React, { useMemo } from 'react';
11
+ import {
12
+ DndContext,
13
+ closestCenter,
14
+ KeyboardSensor,
15
+ PointerSensor,
16
+ useSensor,
17
+ useSensors,
18
+ type DragEndEvent,
19
+ } from '@dnd-kit/core';
20
+ import {
21
+ SortableContext,
22
+ arrayMove,
23
+ sortableKeyboardCoordinates,
24
+ useSortable,
25
+ verticalListSortingStrategy,
26
+ } from '@dnd-kit/sortable';
27
+ import { CSS } from '@dnd-kit/utilities';
28
+ import type {
29
+ RankOrderAnswers,
30
+ RankOrderOption,
31
+ RankOrderOptionDisplay,
32
+ RankOrderQuestion,
33
+ } from '@explorer02/cfm-survey-sdk';
34
+ import {
35
+ assignRankWithoutDuplicates,
36
+ buildRankOrderFromOrderedOptionIds,
37
+ getOrderedOptionIdsFromRanks,
38
+ normalizeRankOrderAnswers,
39
+ } from '@explorer02/cfm-survey-sdk';
40
+
41
+ type RankOrderScaleProps = {
42
+ question: RankOrderQuestion;
43
+ selectedValue?: RankOrderAnswers;
44
+ onSelect: (value: RankOrderAnswers) => void;
45
+ };
46
+
47
+ type RankOrderOptionRowProps = {
48
+ option: RankOrderOption;
49
+ optionDisplay: RankOrderOptionDisplay;
50
+ rankSelect?: React.ReactNode;
51
+ trailingControls?: React.ReactNode;
52
+ dragHandle?: React.ReactNode;
53
+ rankBadge?: number;
54
+ };
55
+
56
+ function RankOrderOptionContent({
57
+ option,
58
+ optionDisplay,
59
+ }: {
60
+ option: RankOrderOption;
61
+ optionDisplay: RankOrderOptionDisplay;
62
+ }) {
63
+ const imageUrl = option.imageUrl || option.previewImageUrl;
64
+
65
+ if (optionDisplay === 'imageOnly') {
66
+ return (
67
+ <div className="flex flex-1 items-center">
68
+ {imageUrl ? (
69
+ <img
70
+ src={imageUrl}
71
+ alt={option.optionLabel || 'Option image'}
72
+ className="h-24 w-24 rounded-md object-cover"
73
+ />
74
+ ) : (
75
+ <div className="flex h-24 w-24 items-center justify-center rounded-md bg-gray-100 text-xs text-gray-400">
76
+ No image
77
+ </div>
78
+ )}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ if (optionDisplay === 'textOnly') {
84
+ return (
85
+ <span
86
+ className="flex-1 text-[15px] font-medium leading-tight text-gray-900"
87
+ dangerouslySetInnerHTML={{ __html: option.optionLabel }}
88
+ />
89
+ );
90
+ }
91
+
92
+ return (
93
+ <div className="flex flex-1 items-center gap-4">
94
+ {imageUrl ? (
95
+ <img
96
+ src={imageUrl}
97
+ alt={option.optionLabel || 'Option image'}
98
+ className="h-14 w-14 shrink-0 rounded-md object-cover"
99
+ />
100
+ ) : (
101
+ <div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-md bg-gray-100 text-xs text-gray-400">
102
+ No image
103
+ </div>
104
+ )}
105
+ <span
106
+ className="text-[15px] font-medium leading-tight text-gray-900"
107
+ dangerouslySetInnerHTML={{ __html: option.optionLabel }}
108
+ />
109
+ </div>
110
+ );
111
+ }
112
+
113
+ function RankOrderOptionRow({
114
+ option,
115
+ optionDisplay,
116
+ rankSelect,
117
+ trailingControls,
118
+ dragHandle,
119
+ rankBadge,
120
+ }: RankOrderOptionRowProps) {
121
+ return (
122
+ <div className="flex items-center gap-3 rounded-lg border border-[#e5e5e5] bg-white px-4 py-3 transition-colors hover:bg-gray-50/50">
123
+ {rankSelect}
124
+ {dragHandle}
125
+ {rankBadge !== undefined && (
126
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#fdf2f8] text-sm font-semibold text-[#e20074]">
127
+ {rankBadge}
128
+ </span>
129
+ )}
130
+ <RankOrderOptionContent option={option} optionDisplay={optionDisplay} />
131
+ {trailingControls}
132
+ </div>
133
+ );
134
+ }
135
+
136
+ function DropdownRankLayout({
137
+ question,
138
+ selectedValue,
139
+ onSelect,
140
+ }: RankOrderScaleProps) {
141
+ const rankAnswers = normalizeRankOrderAnswers(selectedValue);
142
+ const rankChoices = question.options.map((_, index) => index + 1);
143
+
144
+ const handleRankChange = (optionId: string, rawValue: string) => {
145
+ const nextRank = rawValue === '' ? undefined : Number(rawValue);
146
+ onSelect(assignRankWithoutDuplicates(rankAnswers, optionId, nextRank));
147
+ };
148
+
149
+ return (
150
+ <div className="space-y-3">
151
+ {question.options.map(option => {
152
+ const currentRank = rankAnswers[option.id];
153
+
154
+ return (
155
+ <RankOrderOptionRow
156
+ key={option.id}
157
+ option={option}
158
+ optionDisplay={question.optionDisplay}
159
+ rankSelect={
160
+ <select
161
+ aria-label={`Rank for ${option.optionLabel || option.id}`}
162
+ value={currentRank ?? ''}
163
+ onChange={event => handleRankChange(option.id, event.target.value)}
164
+ className="h-10 w-16 shrink-0 rounded border border-gray-300 bg-white px-2 text-sm outline-none focus:border-[#e20074] focus:ring-1 focus:ring-[#e20074]"
165
+ >
166
+ <option value="">-</option>
167
+ {rankChoices.map(rank => (
168
+ <option key={rank} value={rank}>
169
+ {rank}
170
+ </option>
171
+ ))}
172
+ </select>
173
+ }
174
+ />
175
+ );
176
+ })}
177
+ </div>
178
+ );
179
+ }
180
+
181
+ type SortableRankItemProps = {
182
+ option: RankOrderOption;
183
+ optionDisplay: RankOrderOptionDisplay;
184
+ rank: number;
185
+ onMoveUp: () => void;
186
+ onMoveDown: () => void;
187
+ isFirst: boolean;
188
+ isLast: boolean;
189
+ };
190
+
191
+ function SortableRankItem({
192
+ option,
193
+ optionDisplay,
194
+ rank,
195
+ onMoveUp,
196
+ onMoveDown,
197
+ isFirst,
198
+ isLast,
199
+ }: SortableRankItemProps) {
200
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
201
+ id: option.id,
202
+ });
203
+
204
+ const style = {
205
+ transform: CSS.Transform.toString(transform),
206
+ transition,
207
+ opacity: isDragging ? 0.85 : 1,
208
+ zIndex: isDragging ? 10 : undefined,
209
+ };
210
+
211
+ return (
212
+ <div ref={setNodeRef} style={style}>
213
+ <RankOrderOptionRow
214
+ option={option}
215
+ optionDisplay={optionDisplay}
216
+ rankBadge={rank}
217
+ dragHandle={
218
+ <button
219
+ type="button"
220
+ aria-label={`Drag to reorder ${option.optionLabel || option.id}`}
221
+ className="flex h-10 w-8 shrink-0 cursor-grab items-center justify-center rounded text-gray-500 hover:bg-gray-100 active:cursor-grabbing"
222
+ {...attributes}
223
+ {...listeners}
224
+ >
225
+ <svg viewBox="0 0 20 20" className="h-5 w-5" fill="currentColor" aria-hidden="true">
226
+ <circle cx="7" cy="6" r="1.5" />
227
+ <circle cx="13" cy="6" r="1.5" />
228
+ <circle cx="7" cy="10" r="1.5" />
229
+ <circle cx="13" cy="10" r="1.5" />
230
+ <circle cx="7" cy="14" r="1.5" />
231
+ <circle cx="13" cy="14" r="1.5" />
232
+ </svg>
233
+ </button>
234
+ }
235
+ trailingControls={
236
+ <div className="flex shrink-0 flex-col gap-1">
237
+ <button
238
+ type="button"
239
+ aria-label={`Move ${option.optionLabel || option.id} up`}
240
+ disabled={isFirst}
241
+ onClick={onMoveUp}
242
+ className="rounded border border-gray-300 px-2 py-1 text-xs text-gray-700 disabled:cursor-not-allowed disabled:opacity-40"
243
+ >
244
+ Up
245
+ </button>
246
+ <button
247
+ type="button"
248
+ aria-label={`Move ${option.optionLabel || option.id} down`}
249
+ disabled={isLast}
250
+ onClick={onMoveDown}
251
+ className="rounded border border-gray-300 px-2 py-1 text-xs text-gray-700 disabled:cursor-not-allowed disabled:opacity-40"
252
+ >
253
+ Down
254
+ </button>
255
+ </div>
256
+ }
257
+ />
258
+ </div>
259
+ );
260
+ }
261
+
262
+ function DragDropRankLayout({
263
+ question,
264
+ selectedValue,
265
+ onSelect,
266
+ }: RankOrderScaleProps) {
267
+ const rankAnswers = normalizeRankOrderAnswers(selectedValue);
268
+ const optionIds = question.options.map(option => option.id);
269
+
270
+ const orderedOptionIds = useMemo(() => {
271
+ const rankedIds = getOrderedOptionIdsFromRanks(rankAnswers, optionIds);
272
+ const unrankedIds = optionIds.filter(optionId => !rankedIds.includes(optionId));
273
+ return rankedIds.length > 0 ? [...rankedIds, ...unrankedIds] : optionIds;
274
+ }, [rankAnswers, optionIds]);
275
+
276
+ const sensors = useSensors(
277
+ useSensor(PointerSensor),
278
+ useSensor(KeyboardSensor, {
279
+ coordinateGetter: sortableKeyboardCoordinates,
280
+ })
281
+ );
282
+
283
+ const updateOrder = (nextOrder: string[]) => {
284
+ onSelect(buildRankOrderFromOrderedOptionIds(nextOrder));
285
+ };
286
+
287
+ const handleDragEnd = (event: DragEndEvent) => {
288
+ const { active, over } = event;
289
+ if (!over || active.id === over.id) {
290
+ return;
291
+ }
292
+
293
+ const oldIndex = orderedOptionIds.indexOf(String(active.id));
294
+ const newIndex = orderedOptionIds.indexOf(String(over.id));
295
+ if (oldIndex === -1 || newIndex === -1) {
296
+ return;
297
+ }
298
+
299
+ updateOrder(arrayMove(orderedOptionIds, oldIndex, newIndex));
300
+ };
301
+
302
+ const moveOption = (optionId: string, direction: 'up' | 'down') => {
303
+ const currentIndex = orderedOptionIds.indexOf(optionId);
304
+ if (currentIndex === -1) {
305
+ return;
306
+ }
307
+
308
+ const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
309
+ if (targetIndex < 0 || targetIndex >= orderedOptionIds.length) {
310
+ return;
311
+ }
312
+
313
+ updateOrder(arrayMove(orderedOptionIds, currentIndex, targetIndex));
314
+ };
315
+
316
+ return (
317
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
318
+ <SortableContext items={orderedOptionIds} strategy={verticalListSortingStrategy}>
319
+ <div className="space-y-3">
320
+ {orderedOptionIds.map((optionId, index) => {
321
+ const option = question.options.find(item => item.id === optionId);
322
+ if (!option) {
323
+ return null;
324
+ }
325
+
326
+ return (
327
+ <SortableRankItem
328
+ key={option.id}
329
+ option={option}
330
+ optionDisplay={question.optionDisplay}
331
+ rank={index + 1}
332
+ isFirst={index === 0}
333
+ isLast={index === orderedOptionIds.length - 1}
334
+ onMoveUp={() => moveOption(option.id, 'up')}
335
+ onMoveDown={() => moveOption(option.id, 'down')}
336
+ />
337
+ );
338
+ })}
339
+ </div>
340
+ </SortableContext>
341
+ </DndContext>
342
+ );
343
+ }
344
+
345
+ export function RankOrderScale({ question, selectedValue, onSelect }: RankOrderScaleProps) {
346
+ if (question.interactionMode === 'dragAndDrop') {
347
+ return (
348
+ <DragDropRankLayout question={question} selectedValue={selectedValue} onSelect={onSelect} />
349
+ );
350
+ }
351
+
352
+ return <DropdownRankLayout question={question} selectedValue={selectedValue} onSelect={onSelect} />;
353
+ }
@@ -0,0 +1,129 @@
1
+ import type { AnswerValue, NpsScalePoint } from '@explorer02/cfm-survey-sdk';
2
+ import { getAccentColor, npsValueFromOption, starValueFromOption } from './surveyUiScaleUtils';
3
+ import { getEmojiForIndex } from './surveyUiIcons';
4
+
5
+ type RatingScaleProps = {
6
+ questionId: string;
7
+ options: NpsScalePoint[];
8
+ selectedValue?: AnswerValue;
9
+ onSelect: (value: AnswerValue) => void;
10
+ variant?: 'nps' | 'star';
11
+ buttonStyle?: 'standard' | 'numbered' | 'emoji';
12
+ };
13
+
14
+ export default function RatingScale({
15
+ questionId,
16
+ options,
17
+ selectedValue,
18
+ onSelect,
19
+ variant = 'nps',
20
+ buttonStyle = 'numbered',
21
+ }: RatingScaleProps) {
22
+ const resolvedOptions = options.map((option, index) => {
23
+ const value =
24
+ variant === 'star'
25
+ ? starValueFromOption(option, index)
26
+ : npsValueFromOption(option as NpsScalePoint, index);
27
+ const color =
28
+ variant === 'nps' && typeof value === 'number' ? getAccentColor(value) : '#e20074';
29
+ return {
30
+ id: option.id,
31
+ label: option.optionLabel,
32
+ value,
33
+ color,
34
+ };
35
+ });
36
+
37
+ const columnCount = resolvedOptions.length;
38
+ const gridStyle = {
39
+ display: 'grid' as const,
40
+ gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
41
+ gap: '2px',
42
+ };
43
+
44
+ if (buttonStyle === 'emoji') {
45
+ return (
46
+ <div role="radiogroup" className="w-full overflow-x-auto">
47
+ <div className="min-w-0" style={gridStyle}>
48
+ {resolvedOptions.map((option, index) => {
49
+ const isSelected = selectedValue === option.value;
50
+ return (
51
+ <label
52
+ key={`${questionId}-${option.value}`}
53
+ title={option.label}
54
+ className="flex cursor-pointer justify-center"
55
+ >
56
+ <input
57
+ type="radio"
58
+ name={questionId}
59
+ checked={isSelected}
60
+ onChange={() => onSelect(option.value)}
61
+ className="sr-only"
62
+ />
63
+ <div
64
+ className={`flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-lg border-2 text-2xl transition-all ${
65
+ isSelected ? 'border-[#e20074] bg-[#fdf2f8] shadow-md' : 'border-transparent'
66
+ }`}
67
+ >
68
+ {getEmojiForIndex(index, columnCount)}
69
+ </div>
70
+ </label>
71
+ );
72
+ })}
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ return (
79
+ <div role="radiogroup" className="w-full overflow-x-auto">
80
+ <div className="mb-3 min-w-0" style={gridStyle}>
81
+ {resolvedOptions.map(option => (
82
+ <div key={`${questionId}-badge-${option.value}`} className="flex justify-center">
83
+ <div
84
+ className="flex h-5 w-5 sm:h-6 sm:w-6 items-center justify-center rounded-[3px] text-[10px] sm:text-xs font-bold text-white shadow-sm"
85
+ style={{ backgroundColor: option.color }}
86
+ title={option.label}
87
+ >
88
+ {option.label}
89
+ </div>
90
+ </div>
91
+ ))}
92
+ </div>
93
+
94
+ <div className="relative w-full rounded-md bg-[#f2f2f2] px-1 py-2">
95
+ <div className="min-w-0" style={gridStyle}>
96
+ {resolvedOptions.map(option => {
97
+ const isSelected = selectedValue === option.value;
98
+ return (
99
+ <label
100
+ key={`${questionId}-${option.value}`}
101
+ title={option.label}
102
+ className="flex cursor-pointer justify-center"
103
+ >
104
+ <input
105
+ type="radio"
106
+ name={questionId}
107
+ checked={isSelected}
108
+ onChange={() => onSelect(option.value)}
109
+ className="sr-only"
110
+ />
111
+ <div
112
+ className={`flex h-8 w-8 sm:h-10 sm:w-10 items-center justify-center rounded-full border-2 transition-all ${
113
+ isSelected ? 'border-[#e20074] bg-[#fbe8f3]' : 'border-transparent bg-white'
114
+ }`}
115
+ >
116
+ <div
117
+ className={`h-3 w-3 sm:h-4 sm:w-4 rounded-full ${
118
+ isSelected ? 'bg-[#e20074]' : 'bg-gray-300'
119
+ }`}
120
+ />
121
+ </div>
122
+ </label>
123
+ );
124
+ })}
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ }